#!/usr/bin/perl
use strict;
use warnings;
use utf8;
use 5.008_001;
use File::Spec ();
use File::Basename qw/dirname basename/;
use FindBin ();
use lib File::Spec->catdir($FindBin::RealBin, '..', 'share', 'plenv', 'lib', 'perl5');
use File::Path qw(mkpath rmtree);

my $PLENV_HOME;
my $LOCAL = "@{[ File::Spec->rel2abs($FindBin::RealBin) ]}/../share/plenv/";

&main;exit;

# -------------------------------------------------------------------------

sub main {
    my $cmd = shift @ARGV or CMD_help();
    $cmd =~ s/-/_/g;

    my $code = __PACKAGE__->can("CMD_$cmd");
    if ($code) {
        $code->();
    } else {
        die "Unknown command $cmd.";
    }
}

sub home_init {
    if ($ENV{PLENV_HOME}) {
        $PLENV_HOME = $ENV{PLENV_HOME};
    } elsif ($ENV{HOME}) {
        $PLENV_HOME = File::Spec->catdir($ENV{HOME}, ".plenv");
        $ENV{PLENV_HOME} = $PLENV_HOME;
    } else {
        die "There is no ENV[PLENV_HOME] and ENV[HOME]. Please set ENV{PLENV_HOME].";
    }
}

sub CMD_init {
    home_init();
    print <<"...";
export PATH="$PLENV_HOME/shims:\${PATH}"
...
}

sub CMD_rehash {
    home_init();
    rehash();
}

sub CMD_exec {
    home_init();

    my $bin = shift @ARGV or show_help('exec');

    my ($version, $file) = detect_version();

    if ($version eq 'system') {
        # remove shims path from ENV[PATH].
        $ENV{PATH} = join(
            ':', 
            grep { File::Spec->canonpath($_) ne File::Spec->canonpath("$PLENV_HOME/shims/") } File::Spec->path()
        );
    } else {
        my $bindir = "$PLENV_HOME/versions/$version/bin";
        unless (-x File::Spec->catfile($bindir, $bin)) {
            die "[plenv] There is no $bin in $bindir.(determined by @{[ $file || '-' ]})\n";
        }
        $ENV{PATH}="$bindir:$ENV{PATH}";
    }
    exec $bin, @ARGV;
    die $!;
}

sub CMD_which {
    home_init();
    my $bin = shift @ARGV or show_help('which');
    my ($version, $file) = detect_version();

    my @path = grep { File::Spec->canonpath($_) ne File::Spec->canonpath("$PLENV_HOME/shims/") } File::Spec->path();
    if ($version ne 'system') {
        unshift @path, "$PLENV_HOME/versions/$version/bin";
    }

    if (my $fullpath = which($bin, @path)) {
        print "$fullpath\n";
        exit 0;
    } else {
        print "plenv: $bin: command not found\n";
        exit 1;
    }
}

sub which {
    my ($bin, @path) = @_;
    for my $dir (@path) {
        my $fullpath = File::Spec->catfile($dir, $bin);
        if (-x $fullpath) {
            return $fullpath;
        }
    }
    return undef;
}

sub detect_version {
    if ($ENV{PLENV_VERSION}) {
        return ($ENV{PLENV_VERSION});
    }
    if (my $file = find_plenv_version_file()) {
        my $version = slurp_version($file);
        return ($version, $file);
    }
    return ('system');
}

sub CMD_version {
    home_init();

    my ($version, $file) = detect_version();
    print "$version ";
    print "(set by $file)" if $file;
    print "\n";
}

sub slurp_version {
    my $fname = shift;
    open my $fh, '<', $fname or die "$fname: $!";
    my $version = do { local $/; <$fh> };
    $version =~ s/\s//g;
    $version;
}

sub find_plenv_version_file {
    my $file = find_local_plenv_version_file();
    return $file if $file;

    return find_global_plenv_version_file();
}

sub find_local_plenv_version_file {
    my $dir = Cwd::getcwd();
    my %seen;
    while (-d $dir) {
        return undef if $seen{$dir}++; # guard from deep recursion
        if (-f "$dir/.perl-version") {
            return "$dir/.perl-version";
        }
        $dir = dirname($dir);
    }
}

sub find_global_plenv_version_file {
    if (-f "$PLENV_HOME/version") {
        return "$PLENV_HOME/version";
    }
    return undef;
}

sub CMD_help {
    if (@ARGV==1) {
        show_help($ARGV[0]);
    } else {
        require Pod::Usage;
        Pod::Usage::pod2usage(-verbose => 2);
    }
}

sub show_help {
    my $cmd = shift;
    require Pod::Find;
    require Pod::Usage;

    my $input = Pod::Find::pod_where( { -inc => 1, -dirs => [
        File::Spec->catdir($FindBin::RealBin, '..', 'lib')
    ]}, "App::plenv::$cmd" );
    Carp::croak "Unknown subcommand: $input" unless $input;
    Pod::Usage::pod2usage( -verbose => 2, -input => $input );
}

sub CMD_global {
    home_init();

    if (my $version = shift @ARGV) {
        # write it
        write_version_file($version, "$PLENV_HOME/version");
    } else {
        # read from global settings file
        my $version = do {
            if (my $file = find_global_plenv_version_file()) {
                slurp_version($file);
            } else {
                'system'
            }
        };
        print "$version\n";
    }
}

sub CMD_local {
    home_init();

    if (my $version = shift @ARGV) {
        # set it.
        write_version_file($version, "./.perl-version");
    } else {
        if (my $file = find_local_plenv_version_file()) {
            my $version = slurp_version($file);
            print "$version\n";
        } else {
            print "plenv: no local version configured for this directory\n";
        }
    }
}

sub write_version_file {
    my ($version, $versionfile) = @_;
    $version =~ s/\s//g;
    $version =~ s/^perl-//; # remove prefix

    if ($version ne 'system' && !is_installed($version)) {
        die "$version is not installed on plenv.";
    }

    open my $fh, '>', $versionfile
        or die "$versionfile: $!";
    print $fh $version;
    close $fh;
}

sub CMD_install {
    home_init();
    mkpath("$PLENV_HOME/versions");

    if (@ARGV == 0) {
        show_help('install');
        exit(-1);
    }

    install();

    # rehash the ~/.plenv/shims/
    rehash();

    return;
}

sub install {
    my $stuff = shift @ARGV;

    require Perl::Build;
    require Getopt::Long;

    my $installation_name;
    my $test;
    my (@D, @A, @U);
    Getopt::Long::Configure(
        'pass_through',
        'no_ignore_case',
        'bundling',
    );
    Getopt::Long::GetOptions(
        'test' => \$test,
        'as=s', \$installation_name,
        'build-dir=s' => \my $build_dir,
        'D=s@' => \@D,
        'A=s@' => \@A,
        'U=s@' => \@U,
    );

    shift @ARGV if @ARGV >= 1 && $ARGV[0] eq '--';

    my @configure_options = @ARGV;
    unless (@configure_options) {
        push @configure_options, '-de';
    }

    for (@D, @A, @U) {
        s/^=//;
    }

    push @configure_options, map { "-D$_" } @D;
    push @configure_options, map { "-A$_" } @A;
    push @configure_options, map { "-U$_" } @U;

    require POSIX;

    my $remove_build_dir = 0;
    if ($build_dir) {
        # do not remove if user provides --build-dir parameter.
        $remove_build_dir = 0;
        # expand ~ in directory path.
        $build_dir =~ s!~!glob("~")!ge;
    } else {
        $remove_build_dir = 1;
        $build_dir = File::Spec->catdir(File::Spec->tmpdir(), POSIX::strftime("perl-build-%Y%m%d-%H%M%S$$", localtime()));
    }
    print "Creating $build_dir(building directory)\n";
    mkpath($build_dir);

    if ($stuff =~ /\.(gz|bz2)$/) {
        # install from file
        my $dist_tarball_path = $stuff;

        $installation_name ||= do {
            my $name = basename($dist_tarball_path);
            $name =~ s!\.tar\..+$!!; # remove ext
            $name =~ s!^perl-!!; # remove prefix
            $name;
        };

        if (is_installed( $installation_name )) {
            die "\nABORT: $installation_name is already installed.\n\n";
        }

        # -de means "use default settings without interactive questions"
        my $dst_path = File::Spec->catdir($PLENV_HOME, 'versions', $installation_name);
        Perl::Build->install_from_tarball(
            $dist_tarball_path => (
                build_dir => $build_dir,
                dst_path => $dst_path,
                configure_options => \@configure_options,
                test => $test,
            )
        );

        Perl::Build->symlink_devel_executables(
            File::Spec->catdir($dst_path, 'bin'),
        );
    } else {
        # install from CPAN
        my $version = $stuff;
        $version =~ s!^perl-!!;
        $installation_name ||= $version;

        if (is_installed( $installation_name )) {
            die "\nABORT: $installation_name is already installed.\n\n";
        }

        if ($version =~ /^5\.1[13579]\./) {
            push @configure_options, '-Dusedevel';
        }

        my $dst_path = File::Spec->catdir($PLENV_HOME, 'versions', $installation_name);
        print "Install $version to $dst_path\n";
        Perl::Build->install_from_cpan(
            $version => (
                # tarball_dir => File::Spec->catdir($PLENV_HOME, 'dists'),
                build_dir => $build_dir,
                dst_path => $dst_path,
                configure_options => \@configure_options,
                test => $test,
            )
        );
        Perl::Build->symlink_devel_executables(
            File::Spec->catdir($dst_path, 'bin'),
        );
    }

    # build dir was not useful after install successfully.
    # And, do not remove working directory when user specified.
    # Because hackers need to save object file for debugging with gdb.
    if ($remove_build_dir) {
        rmtree($build_dir);
    }
}

sub is_installed {
    my ($name) = @_;
    return grep { $name eq $_ } installed_perls();
}

sub CMD_list {
    home_init();
    my ($current, ) = detect_version();
    for my $version (installed_perls(), 'system') {
        print $version eq $current ? "* " : "  ";
        print "$version\n"
    }
}

sub CMD_versions { CMD_list(@_) } # alias

sub CMD_list_modules {
    home_init();
    my $cmd = sprintf(q{"%s" exec perl -MExtUtils::Installed -e "print qq!\$_$/! for ExtUtils::Installed->new->modules"}, File::Spec->rel2abs($0));
    exec $cmd;
}

sub installed_perls {
    my $self    = shift;

    my @result;
    for (<$PLENV_HOME/versions/*>) {
        my ($name) = $_ =~ m/\/([^\/]+$)/;
        my $executable = File::Spec->catfile($_, 'bin', 'perl');

        push @result, $name;
    }

    return @result;
}

sub rehash {
    mkpath("$PLENV_HOME/shims");

    my %seen;
    for my $bin (map { basename($_) } grep { -x $_ } <$PLENV_HOME/versions/*/bin/*>) {
        next if $seen{$bin}++;

        my $shimbin = File::Spec->catfile($PLENV_HOME, 'shims', $bin);

        open my $fh, '>', $shimbin or die "$shimbin: $!";
        my $cmd = do {
            if ($bin eq 'cpanm') {
                join("\n",
                    sprintf(q{"%s" exec "$program" "$@"}, File::Spec->rel2abs($0)),
                    sprintf(q{"%s" rehash}, File::Spec->rel2abs($0))
                )
            } else {
                sprintf(q{exec "%s" exec "$program" "$@"}, File::Spec->rel2abs($0))
            }
        };
        print $fh sprintf(<<'...', $PLENV_HOME, $cmd);
#!/usr/bin/env bash
set -e
[ -n "$PLENV_DEBUG" ] && set -x

program="${0##*/}"

export PLENV_HOME="%s"
%s
...
        close $fh;

        chmod 0755, $shimbin or die "$shimbin: $!";
    }
}

sub CMD_install_cpanm {
    print("Installing cpanm to current perl\n");
    home_init();
    my ($version, $file) = detect_version();
    install_cpanm($version);
    rehash();
}

sub CMD_available {
    require Perl::Build;
    my @available = Perl::Build->available_perls();
    print $_, "\n" for @available;
}

sub CMD_migrate_modules {
    home_init();
    @ARGV==2 or show_help('migrate-modules');
    my ($src, $dst) = @ARGV;

    my $srcperl = "${PLENV_HOME}/versions/${src}/bin/perl";
    my $dstcpanm = "${PLENV_HOME}/versions/${dst}/bin/cpanm";
    install_cpanm($dst);
    open my $srcfh, "-|", $srcperl, '-MExtUtils::Installed', '-e', 'print $_, "\n" for ExtUtils::Installed->new->modules'
        or die "Cannot exec: $!";
    open my $dstfh, "|-", $dstcpanm
        or die "Cannot exec $dstcpanm: $!";
    while (<$srcfh>) {
        print $dstfh $_;
    }
    close $srcfh;
    close $dstfh;
}

sub install_cpanm {
    my ($version) = @_;
    my $perl = "${PLENV_HOME}/versions/${version}/bin/perl";
    my $cpanm = _find_cpanm_path();
    system("$perl $cpanm App::cpanminus")
        == 0 or die "Cannot install cpanm to $perl";
}

sub _find_cpanm_path {
    {
        my $cpanm = File::Spec->catfile($FindBin::RealBin, '..', 'share/plenv/bin/cpanm');
        return $cpanm if -f $cpanm;
    }
    {
        my $cpanm = $^X;
        $cpanm =~ s!/perl([0-9\.]*)$!/cpanm!;
        return $cpanm if -f $cpanm;
    }
    die "[ABORT] Cannot find bundled `cpanm` command\n";
}

__END__

=encoding utf8

=head1 NAME

plenv - perl binary manager

=head1 SYNOPSIS

    plenv help

    # list available perl versions
    plenv available

    # install perl5 binary
    plenv install 5.16.2 -Dusethreads

    # execute command on current perl
    plenv exec ack

    # change global default perl to 5.16.2
    plenv global 5.16.2

    # change local perl to 5.14.0
    plenv local 5.14.0

    # run this command after install cpan module, contains executable script.
    plenv rehash

    # install cpanm to current perl
    plenv install-cpanm

    # migrate modules(install all installed modules for 5.8.9 to 5.16.2 environment.)
    plenv migrate-modules 5.8.9 5.16.2

    # locate a program file in the plenv's path
    plenv which cpanm

=head1 DESCRIPTION

Use plenv to pick a Perl version for your application and guarantee
that your development environment matches production. Put plenv to work
with [Carton](http://github.com/miyagawa/carton/) for painless Perl upgrades and bulletproof deployments.

=head1 plenv vs. perlbrew

plenv supports project local version determination.

i.e. .perl-version file support.

=head1 INSTALLATION

=head2 INSTALL FROM CPAN

Install plenv with CPAN.

    $ sudo -H cpan -i App::plenv

=head2 INSTALL FROM Homebrew

You can use homebrew to install plenv.

    $ brew install plenv

=head2 INSTALL WITH GIT

1. Check out plenv into ~/.plenv/

    $ git clone git://github.com/tokuhirom/plenv.git ~/.plenv

2. Add ~/.plenv/bin/ to your $PATH for access to the `plenv` command-line utility.

    $ echo 'export PATH="$HOME/.plenv/bin:$PATH"' >> ~/.bash_profile

    **Ubuntu note**: Modify your `~/.profile` instead of `~/.bash_profile`.

    **Zsh note**: Modify your `~/.zshrc` file instead of `~/.bash_profile`.

=head1 SETUP SHELL SETTINGS

=over 4

=item Add `plenv init` to your shell to enable shims and autocompletion.

    $ echo 'eval "$(plenv init -)"' >> ~/.bash_profile

I<Same as in previous step, use `~/.profile` on Ubuntu, `~/.zshrc` for Zsh.>

=item Restart your shell as a login shell so the path changes take effect.

You can now begin using plenv.

    $ exec $SHELL -l

=back

=head1 Perl version detection

plenv detects current perl version with following order.

=over 4

=item PLENV_VERSION environment variable

=item .perl-version file in current and upper directories.

=item global settings(~/.plenv/version)

=item use system perl

=back

=head1 DEPENDENCIES

  * Perl 5.8.1+
  * wget or curl or fetch.

=head1 FAQ

=over 4

=item How can I install cpanm?

Try to use following command.

    % plenv install-cpanm

This command install cpanm to current environment.

=item What should I do for installing the module which I used for new Perl until now? 

You can use C< migrate-modules > subcommand.

    % plenv migrate-modules 5.8.2 5.16.2

It make a list of installed modules in 5.8.2, and install these modules to 5.16.2 environment.

=item How can I enable -g option without slowing down binary?

Use following command.

    % plenv install 5.16.2 -DDEBUGGING=-g

=back

=head1 BUG REPORTING

Plese use github issues: L<http://github.com/tokuhirom/plenv/>.

=head1 AUTHOR

Tokuhiro Matsuno E<lt>tokuhirom AAJKLFJEF@ GMAIL COME<gt>

=head1 SEE ALSO

L<App::perlbrew> provides same feature. But plenv provides project local file: B< .perl-version >.

Most of part was inspired from L<rbenv|https://github.com/sstephenson/rbenv>.

=head1 LICENSE

Copyright (C) Tokuhiro Matsuno

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

=cut
