#!/usr/bin/env perl

# nodebrew
# Node.js version manager
#
# @author  Kazuhito Hokamura <k.hokamura@gmail.com>
# @url     https://github.com/hokaccha/nodebrew

use strict;
use warnings;

package Nodebrew;

use File::Path qw/rmtree mkpath/;
use File::Basename qw/basename/;

our $VERSION = '1.2.0';

sub new {
    my $class = shift;
    my %opt = @_;
    my $self = {};
    my @props = qw/
        brew_dir
        nodebrew_url
        bash_completion_url
        zsh_completion_url
        fish_completion_url
        fetcher
        remote_list_url
        tarballs
        tarballs_binary
    /;

    for (@props) {
        if (defined $opt{$_}) {
            $self->{$_} = $opt{$_};
        }
        else {
            die "required $_";
        }
    }

    bless $self, $class;
    $self->init();

    return $self;
}

sub init {
    my $self = shift;

    $self->{src_dir} = $self->{brew_dir} . '/src';
    $self->{node_dir} = $self->{brew_dir} . '/node';
    $self->{iojs_dir} = $self->{brew_dir} . '/iojs';
    $self->{current} = $self->{brew_dir} . '/current';
    $self->{default_dir} = $self->{brew_dir} . '/default';
    $self->{alias_file} = $self->{brew_dir} . '/alias';
    $self->{bash_completion_dir} = $self->{brew_dir} . '/completions/bash';
    $self->{zsh_completion_dir} = $self->{brew_dir} . '/completions/zsh';
    $self->{fish_completion_dir} = $self->{brew_dir} . '/completions/fish';
}

sub run {
    my ($self, $command, $args) = @_;

    $command ||= '';
    $command =~ s/-/_/g;

    if (my $cmd = $self->can("_cmd_$command")) {
        $cmd->($self, $args);
    }
    else {
        $self->_cmd_help($args);
    }
}

sub _cmd_use {
    my ($self, $args) = @_;

    my $version = $self->find_available_version($args->[0]);
    my $target = $self->get_install_dir . "/$version";
    my $nodebrew_path = "$target/bin/nodebrew";

    unlink $self->{current} if -l $self->{current};
    symlink $target, $self->{current};
    symlink "$self->{brew_dir}/nodebrew", $nodebrew_path unless -l $nodebrew_path;
    my $prefix = $self->is_iojs ? 'io@' : '';
    print "use $prefix$version\n";
}

sub _cmd_install {
    my ($self, $args) = @_;

    my ($version, $release) = $self->find_install_version($args->[0]);
    my ($platform, $arch) = Nodebrew::Utils::system_info();
    if ($arch eq 'armv6l' && $version =~ m/v0\.\d+\.\d+/) {
        # nodev0.x.x on armv6l(RaspberryPi1)
        $arch = 'arm-pi';
    }
    my $tarball_url = $self->get_tarball($self->get_tarballs_binary_url, $version, {
        platform => $platform,
        arch     => $arch,
        release  => $release || "release"
    });
    my $type = $self->get_type();
    my $src_dir = "$self->{src_dir}/$version";
    my $target_name = "$type-$version-$platform-$arch";
    my $tarball_path = "$src_dir/$target_name.tar.gz";

    $self->clean($version);
    mkdir $src_dir;

    print "Fetching: $tarball_url\n";
    $self->{fetcher}->download($tarball_url, $tarball_path)
        or error_and_exit("download failed: $tarball_url");

    Nodebrew::Utils::extract_tar($tarball_path, $src_dir);

    # https://github.com/hokaccha/nodebrew/issues/34
    if ($type eq 'iojs' && $platform eq 'linux' && $arch eq 'x86') {
        $target_name =~ s/x86$/ia32/;
    }

    rename "$src_dir/$target_name", $self->get_install_dir . "/$version" or die "Error: $!";

    print "Installed successfully\n";
}

sub _cmd_install_binary {
    my ($self, $args) = @_;

    $self->_cmd_install($args);
}

sub _cmd_compile {
    my ($self, $args) = @_;

    my $v = shift @$args;
    my $configure_opts = join ' ', @$args;
    my ($version, $release) = $self->find_install_version($v);
    my $opts = +{ release => $release || "release" };
    my $tarball_url = $self->get_tarball($self->get_tarballs_url, $version, $opts);
    my $src_dir = "$self->{src_dir}/$version";
    my $target_name = $self->get_type . "-$version";
    my $tarball_path = "$src_dir/$target_name.tar.gz";

    $self->clean($version);
    mkdir $src_dir;

    print "Fetching: $tarball_url\n";
    $self->{fetcher}->download($tarball_url, $tarball_path)
        or error_and_exit("download failed: $tarball_url");

    Nodebrew::Utils::extract_tar($tarball_path, $src_dir);

    my $install_dir = $self->get_install_dir;
    system qq[
        cd "$src_dir/$target_name" &&
        ./configure --prefix="$install_dir/$version" $configure_opts &&
        make &&
        make install
    ];
}

sub _cmd_uninstall {
    my ($self, $args) = @_;
    my $version = $self->normalize_version($args->[0]);
    my $target = $self->get_install_dir . "/$version";
    my $current_version = $self->get_current_version();

    error_and_exit("$version is not installed") unless -e $target;
    rmtree $target;

    $version = "io\@$version" if ($self->is_iojs);
    if ($current_version eq $version) {
        $self->use_default();
    }

    print "$version uninstalled\n";
}

sub _cmd_list {
    my ($self, $args) = @_;

    my @node_versions = @{$self->get_local_version('node')};
    my @iojs_versions = map { "io\@$_"; } @{$self->get_local_version('iojs')};
    my @versions;
    push @versions, @node_versions, @iojs_versions;

    print scalar @versions
        ? join("\n", @{Nodebrew::Utils::sort_version(\@versions)})
        : "not installed";
    print "\n\ncurrent: " . $self->get_current_version() . "\n";
}

sub _cmd_prune {
    my ($self, $args) = @_;

    my $dry_run = grep { $_ eq '--dry-run' } @$args;

    my @node_versions = @{$self->get_local_version('node')};
    my @iojs_versions = map { "io\@$_"; } @{$self->get_local_version('iojs')};
    my @versions;
    push @versions, @node_versions, @iojs_versions;

    my %major_map;
    for my $version (@versions) {
        my($variant, $major) = $version =~ m/([^@]+@)?(v\d+)/;
        $variant ||= "";
        push @{$major_map{"$variant$major"} ||= []}, $version;
    }

    my @keeping;
    my @removing;
    for my $major (@{Nodebrew::Utils::sort_version([keys %major_map])}) {
        my @vers = @{Nodebrew::Utils::sort_version($major_map{$major})};
        my $latest_ver = pop @vers;
        push @keeping, $latest_ver;
        push @removing, @vers;

        print "$major:\n";
        print "  keeping: $latest_ver\n";
        print "  removing: [" . join(", ", @vers) . "]\n";
    }

    if (!$dry_run) {
        for my $version (@removing) {
            $self->_cmd_uninstall([$version]);
        }
    }
}

sub _cmd_ls {
    my ($self, $args) = @_;

    $self->_cmd_list($args);
}

sub _cmd_ls_remote {
    my ($self, $args) = @_;

    my @node_versions = @{Nodebrew::Utils::sort_version($self->get_remote_version('node'))};
    my @iojs_versions = map { "io\@$_"; } @{Nodebrew::Utils::sort_version($self->get_remote_version('iojs'))};
    my @versions;
    push @versions, @node_versions, @iojs_versions;

    my $i = 0;
    my %tmp;

    for (@versions) {
        my ($v1, $v2, $v3) = $_ =~ m/v(\d+)\.(\d+)\.(\d+)/;
        if ($v1 == 0 && !$tmp{"$v1.$v2"}++) {
            print "\n\n" if $i;
            $i = 0;
        }
        elsif ($v1 != 0 && !$tmp{"$v1"}++) {
            print "\n\n" if $i;
            $i = 0;
        }

        print $_;
        print ++$i % 8 == 0 ? "\n" : ' ' x (10 - length $_);
    }
    print "\n";
}

sub _cmd_ls_all {
    my ($self, $args) = @_;

    print "remote:\n";
    $self->_cmd_ls_remote($args);
    print "\nlocal:\n";
    $self->_cmd_ls($args);
}

sub _cmd_alias {
    my ($self, $args) = @_;

    my ($key, $val)  = $args ? @$args : ();
    my $alias = Nodebrew::Config->new($self->{alias_file});

    # set alias
    if ($key && $val) {
        $alias->set($key, $val);
        $alias->save();
        print "$key -> $val\n";
    }
    # get alias
    elsif ($key) {
        $val = $alias->get($key);
        print $val ? "$key -> $val\n" : "$key is not set alias\n";
    }
    # get alias all
    else {
        my $datas = $alias->get_all();
        for (keys %{$datas}) {
            print $_ . ' -> ' . $datas->{$_} . "\n";
        }
    }
}

sub _cmd_unalias {
    my ($self, $args) = @_;

    my $alias = Nodebrew::Config->new($self->{alias_file});
    my $key = $args->[0];
    if (!$key) {
        return;
    }

    if ($alias->del($key)) {
        $alias->save();
        print "Removed $key\n";
    }
    else {
        error_and_exit("$key is not defined");
    }
}

sub _cmd_setup {
    my ($self, $args) = @_;

    $self->_cmd_setup_dirs();

    my $nodebrew_path = "$self->{brew_dir}/nodebrew";
    $self->fetch_nodebrew();
    `chmod +x $nodebrew_path`;
    symlink $nodebrew_path, "$self->{default_dir}/bin/nodebrew";
    $self->use_default() if $self->get_current_version() eq 'none';

    my $brew_dir = $self->{brew_dir};
    $brew_dir =~ s/$ENV{'HOME'}/\$HOME/;
    print "Installed nodebrew in $brew_dir\n\n";
    print "========================================\n";
    print "Export a path to nodebrew:\n\n";
    print "export PATH=$brew_dir/current/bin:\$PATH\n";
    print "========================================\n";
}

sub _cmd_setup_dirs {
    my $self = shift;

    mkdir $self->{brew_dir} unless -e $self->{brew_dir};
    mkdir $self->{src_dir} unless -e $self->{src_dir};
    mkdir $self->{node_dir} unless -e $self->{node_dir};
    mkdir $self->{iojs_dir} unless -e $self->{iojs_dir};
    mkdir $self->{default_dir} unless -e $self->{default_dir};
    mkdir "$self->{default_dir}/bin" unless -e "$self->{default_dir}/bin";
}

sub _cmd_clean {
    my ($self, $args) = @_;

    my $version = $self->normalize_version($args->[0]);

    $self->clean($version);
    print "Cleaned $version\n";
}

sub _cmd_selfupdate {
    my ($self, $args) = @_;

    $self->fetch_nodebrew();
    print "Updated successfully\n";
}

sub _cmd_migrate_package {
    my ($self, $args) = @_;

    my $current_version = $self->get_current_version();
    my $is_iojs = $current_version =~ s/^io@//;
    error_and_exit("version not selected") if $current_version eq 'none';

    my $current_type = $is_iojs ? 'iojs' : 'node';
    my @current_packages = $self->get_packages($current_version, $current_type);

    my $version = $self->find_available_version($args->[0]);
    my @target_packages = $self->get_packages($version);
    my $package_dir = $self->get_install_dir . "/$version/lib/node_modules";

    my (@success, @fail);
    foreach my $package_name (@target_packages) {
        if (grep { $_ eq $package_name } @current_packages) {
            print "$package_name is already installed\n";
            next;
        }

        print "Try to install $package_name ...\n";
        my $result = system qq[npm install -g $package_dir/$package_name];
        if ($result) {
            push @fail, $package_name;
        }
        else {
            push @success, $package_name;
        }
    }

    if (@success) {
        print "\nInstalled successfully:\n", join( "\n", @success ), "\n\n";
    }
    if (@fail) {
        print "\nFailed installation:\n", join( "\n", @fail ), "\n\n";
    }
}

sub _cmd_exec {
    my ($self, $args) = @_;

    my $version = $self->find_available_version(shift @$args);

    $ENV{PATH} = $self->get_install_dir . "/$version/bin:$ENV{PATH}";

    shift @$args if $args->[0] eq '--';
    my $command = join ' ', @$args;

    system $command;
    exit $? >> 8;
}

sub _cmd_help {
    my ($self, $args) = @_;

    print <<"...";
nodebrew $VERSION

Usage:
    nodebrew help                         Show this message
    nodebrew install <version>            Download and install <version> (from binary)
    nodebrew compile <version>            Download and install <version> (from source)
    nodebrew install-binary <version>     Alias of `install` (For backward compatibility)
    nodebrew uninstall <version>          Uninstall <version>
    nodebrew use <version>                Use <version>
    nodebrew list                         List installed versions
    nodebrew ls                           Alias for `list`
    nodebrew ls-remote                    List remote versions
    nodebrew ls-all                       List remote and installed versions
    nodebrew alias <key> <value>          Set alias
    nodebrew unalias <key>                Remove alias
    nodebrew clean <version> | all        Remove source file
    nodebrew selfupdate                   Update nodebrew
    nodebrew migrate-package <version>    Install global NPM packages contained in <version> to current version
    nodebrew exec <version> -- <command>  Execute <command> using specified <version>
    nodebrew prune [--dry-run]            Uninstall old versions, keeping the latest version for each major version

Example:
    # install
    nodebrew install v8.9.4

    # use a specific version number
    nodebrew use v8.9.4
...
}

sub get_nightly {
    my ($self, $version) = @_;

    my $type = $self->get_type();
    my $url = $self->get_remote_list_url($type, $version);
    my $html = $self->{fetcher}->fetch($url);
    my $latest;
    while ($html =~ m/(v\d+\.\d+\.\d+-$version.*)\/"/g) {
      $latest = $1;
    }
    return ($latest, $version);
}

sub find_install_version {
    my ($self, $v) = @_;

    my $version = $self->normalize_version($v);
    my $release;

    if ($version eq 'nightly' || $version eq 'next-nightly') {
        ($version, $release) = $self->get_nightly($version);
    } elsif ($version !~ m/v\d+\.\d+\.\d+/) {
        ($version, $release) = Nodebrew::Utils::find_version(
            $version, $self->get_remote_version(undef, $version)
        );
    }

    error_and_exit('version not found') unless $version;
    error_and_exit("$version is already installed")
        if -e $self->get_install_dir . "/$version";

    return ($version, $release);
}

sub find_available_version {
    my ($self, $arg) = @_;

    my $alias = Nodebrew::Config->new($self->{alias_file});
    my $target_version = $self->normalize_version($alias->get($arg) || $arg);
    my $local_version = $self->get_local_version();
    my $version = Nodebrew::Utils::find_version($target_version, $local_version)
        or error_and_exit("$target_version is not installed");

    return $version;
}

sub get_tarball {
    my ($self, $tarballs, $version, $vars) = @_;

    my $tarball;
    my $msg = '';

    $vars ||= {};
    $vars->{version} = $version;
    $vars->{release} ||= "release";

    for (@$tarballs) {
        my $url = Nodebrew::Utils::apply_vars($_, $vars);
        if ($self->{fetcher}->fetch_able($url)) {
            $tarball = $url;
            last;
        }
        else {
            $msg .= "\nCan not fetch: $url";
        }
    }

    error_and_exit("$version is not found\n$msg") unless $tarball;

    return $tarball;
}

sub clean {
    my ($self, $version) = @_;

    if ($version eq 'all') {
        opendir my $dh, $self->{src_dir} or return;
        while (my $file = readdir $dh) {
            next if $file =~ m/^\./;
            my $path = "$self->{src_dir}/$file";
            unlink $path if -f $path;
            rmtree $path if -d $path;
        }
    }
    elsif (-d "$self->{src_dir}/$version") {
        rmtree "$self->{src_dir}/$version";
    }
}

sub is_iojs {
    my $self = shift;

    return $self->{iojs};
}

sub is_node {
    my $self = shift;

    return !$self->{iojs};
}

sub get_type {
    my $self = shift;

    return $self->is_iojs ? 'iojs' : 'node';
}

sub use_default {
    my $self = shift;

    unlink $self->{current} if -l $self->{current};
    symlink $self->{default_dir}, $self->{current};
}

sub get_current_version {
    my $self = shift;

    return 'none' unless -l $self->{current};
    my $current_version = readlink $self->{current};

    return $1 if $current_version =~ m!^$self->{node_dir}/(.+)!;
    return "io\@$1" if $current_version =~ m!^$self->{iojs_dir}/(.+)!;
    return 'none';
}

sub fetch_nodebrew {
    my $self = shift;

    print "Fetching nodebrew...\n";
    my $nodebrew_source = $self->{fetcher}->fetch($self->{nodebrew_url});
    my $bash_completion = $self->{fetcher}->fetch($self->{bash_completion_url});
    my $zsh_completion = $self->{fetcher}->fetch($self->{zsh_completion_url});
    my $fish_completion = $self->{fetcher}->fetch($self->{fish_completion_url});
    my $nodebrew_path = "$self->{brew_dir}/nodebrew";
    my $bash_completion_path = $self->{bash_completion_dir} . '/' . basename($self->{bash_completion_url});
    my $zsh_completion_path = $self->{zsh_completion_dir} . '/' . basename($self->{zsh_completion_url});
    my $fish_completion_path = $self->{fish_completion_dir} . '/' . basename($self->{fish_completion_url});

    mkpath $self->{bash_completion_dir} unless -e $self->{bash_completion_dir};
    mkpath $self->{zsh_completion_dir} unless -e $self->{zsh_completion_dir};
    mkpath $self->{fish_completion_dir} unless -e $self->{fish_completion_dir};

    $self->make_file($nodebrew_source, $nodebrew_path);
    $self->make_file($bash_completion, $bash_completion_path);
    $self->make_file($zsh_completion, $zsh_completion_path);
    $self->make_file($fish_completion, $fish_completion_path);
}

sub make_file {
    my ($self, $content, $dest) = @_;
    open my $fh, '>', $dest or die "Error: $!";
    print $fh $content;
}

sub get_local_version {
    my ($self, $type) = @_;

    my @versions;
    opendir my $dh, $self->get_install_dir($type) or die $!;
    while (my $dir = readdir $dh) {
        push @versions, $dir unless $dir =~ '^\.\.?$';
    }

    return \@versions;
}

sub get_remote_list_url {
    my ($self, $type, $release) = @_;

    my $url = $self->{remote_list_url}->{$type || $self->get_type};
    $release ||= 'release';
    my $opt = +{ 'release' => $release };
    if ($release eq 'nightly' || $release eq 'next-nightly') {
      $opt = +{ 'release' => $release };
    }
    return Nodebrew::Utils::apply_vars($url, $opt);
}

sub get_tarballs_url {
    my $self = shift;

    return $self->{tarballs}->{$self->get_type}
}

sub get_tarballs_binary_url {
    my $self = shift;

    return $self->{tarballs_binary}->{$self->get_type}
}

sub get_install_dir {
    my ($self, $type) = @_;

    my $is_iojs = ($type && $type eq 'iojs') || $self->is_iojs;
    my $dir = $is_iojs ? $self->{iojs_dir} : $self->{node_dir};

    mkdir $dir unless -e $dir;

    return $dir;
}

sub get_remote_version {
    my ($self, $type, $version) = @_;

    my $url = $self->get_remote_list_url($type, $version);
    my $html = $self->{fetcher}->fetch($url);
    my @versions;
    my %tmp;
    while ($html =~ m/(\d+\.\d+\.\d+)/g) {
        my $v = "v$1";
        push @versions, $v unless $tmp{$v}++;
    }

    return \@versions;
}

sub get_packages {
    my ($self, $version, $type) = @_;

    my $install_dir = $self->get_install_dir($type);
    my $module_dir = "$install_dir/$version/lib/node_modules";
    my @packages;

    opendir my $dh, $module_dir or die $!;
    while (my $dir = readdir $dh) {
        push @packages, $dir unless $dir =~ /^\./;
    }

    return @packages;
}

sub error_and_exit {
    my $msg = shift;

    print "$msg\n";
    exit 1;
}

sub normalize_version {
    my ($self, $v) = @_;

    error_and_exit('version is required') unless $v;

    if ($self->is_node) {
        $self->{iojs} = $v =~ s/^io@//;
    }

    return $v =~ m/^\d+\.?(\d+|x)?\.?(\d+|x)?$/ ? "v$v" : $v;
}

package Nodebrew::Utils;

use POSIX;
use Cwd 'getcwd';

sub sort_version {
    my $version = shift;

    return [sort {
        my ($a0, $a1, $a2, $a3) = ($a =~ m/([^@]+@)?v(\d+)(?:\.(\d+)\.(\d+))?/);
        my ($b0, $b1, $b2, $b3) = ($b =~ m/([^@]+@)?v(\d+)(?:\.(\d+)\.(\d+))?/);
        $a0 ||= ""; $a1 ||= 0; $a2 ||= 0; $a3 ||= 0;
        $b0 ||= ""; $b1 ||= 0; $b2 ||= 0; $b3 ||= 0;
        $a0 cmp $b0 || $a1 <=> $b1 || $a2 <=> $b2 || $a3 <=> $b3
    } @$version];
}

sub find_version {
    my ($version, $versions) = @_;

    $versions = Nodebrew::Utils::sort_version($versions);
    my @versions = @$versions;

    return undef unless scalar @versions;
    return pop @versions if $version eq 'latest';

    if ($version eq 'stable') {
        for (reverse @versions) {
            my ($major) = m/^v(\d+)/;
            return $_ if $major % 2 == 0;
        }
        return;
    }

    my @v = map {
        $_ && $_ eq 'x' ? undef : $_
    } $version =~ m/^v(\d+)\.?(\d+|x)?\.?(\d+|x)?$/;

    my @ret;
    if (defined($v[0]) && defined($v[1]) && defined($v[2])) {
        @ret = grep { /^v?$v[0]\.$v[1]\.$v[2]$/ } @versions;
    }
    elsif (defined($v[0]) && defined($v[1]) && !defined($v[2])) {
        @ret = grep { /^v?$v[0]\.$v[1]\./ } @versions;
    }
    elsif (defined($v[0]) && !defined($v[1])) {
        @ret = grep { /^v?$v[0]\./ } @versions;
    }

    pop @ret;
}

sub parse_args {
    my $command = shift;

    return ($command, \@_);
}

sub system_info {
    my $arch;
    my ($sysname, $machine) = (POSIX::uname)[0, 4];

    if  ($machine =~ m/x86_64/) {
        $arch = 'x64';
    } elsif ($machine =~ m/arm64/) {
        $arch = 'arm64';
    } elsif ($machine =~ m/i\d86/) {
        $arch = 'x86';
    } elsif ($machine =~ m/armv6l/) {
        $arch = 'armv6l';
    } elsif ($machine =~ m/armv7l/) {
        $arch = 'armv7l';
    } elsif ($machine =~ m/aarch64/) {
        $arch = 'armv7l';
    } elsif ($sysname =~ m/sunos/i) {
        # SunOS $machine => 'i86pc'. but use 64bit kernel.
        # Solaris 11 not support 32bit kernel.
        # both 32bit and 64bit node-binary even work on 64bit kernel
        $arch = 'x64';
    } else {
        die "Error: $sysname $machine is not supported."
    }

    return (lc $sysname, $arch);
}

sub apply_vars {
    my ($str, $hash) = @_;

    for my $key (keys %$hash) {
        my $val = $hash->{$key};

        $str =~ s/#\{$key\}/$val/g;
    }

    return $str;
}

sub extract_tar {
    my ($filepath, $outdir) = @_;

    my $cwd = getcwd;

    chdir($outdir);

    eval {
        require Archive::Tar;
        my $tar = Archive::Tar->new;
        $tar->read($filepath);
        $tar->extract;
    };
    if ($@) {
        `tar zfx $filepath`;
    }

    chdir($cwd);
}

package Nodebrew::Config;

sub new {
    my ($class, $file) = @_;

    my $data = {};
    if (-e $file) {
        open my $fh, '<', $file or die "Error: $!";
        my $str = do { local $/; <$fh> };
        close $fh;
        $data = Nodebrew::Config::_parse($str);
    }

    bless { file => $file, data => $data }, $class;
}

sub get_all {
    my $self = shift;

    return $self->{data};
}

sub get {
    my ($self, $key) = @_;

    return $self->{data}->{$key};
}

sub set {
    my ($self, $key, $val) = @_;

    if ($key && $val) {
        $self->{data}->{$key} = $val;
        return 1;
    }

    return;
}

sub del {
    my ($self, $key) = @_;

    if ($key && $self->get($key)) {
        delete $self->{data}->{$key};
        return 1;
    }
    return;
}

sub save {
    my $self = shift;

    open my $fh, '>', $self->{file} or die "Error: $!";
    print $fh Nodebrew::Config::_strigify($self->{data});
    close $fh;

    return 1;
}

sub _parse {
    my $str = shift;

    my %ret;
    for (split /\n/, $str) {
        my ($key, $val) = ($_ =~ m/^\s*(.*?)\s*=\s*(.*?)\s*$/);
        $ret{$key} = $val if $key;
    }
    return \%ret;
}

sub _strigify {
    my $datas = shift;

    my $ret = '';
    for (keys %$datas) {
        $ret .= $_ . ' = ' . $datas->{$_} . "\n";
    }

    return $ret;
}

package Nodebrew::Fetcher;

sub get {
    my $type = shift;

    $type eq 'wget' ? Nodebrew::Fetcher::wget->new:
    $type eq 'curl' ? Nodebrew::Fetcher::curl->new:
    die 'Fetcher type invalid';
}

package Nodebrew::Fetcher::curl;

sub new { bless {}; }

sub fetch_able {
    my ($self, $url) = @_;

    `curl -LIs "$url"` =~ m/200 OK|HTTP\/2 200/;
}

sub fetch {
    my ($self, $url) = @_;

    `curl -Ls $url`;
}

sub download {
    my ($self, $url, $path) = @_;

    system("curl -C - --progress-bar $url -o $path") == 0;
}

package Nodebrew::Fetcher::wget;

sub new { bless {}; }

sub fetch_able {
    my ($self, $url) = @_;

    `wget -Sq --spider "$url" 2>&1` =~ m/200 OK/;
}

sub fetch {
    my ($self, $url) = @_;

    `wget -q $url -O -`;
}

sub download {
    my ($self, $url, $path) = @_;

    system("wget -c $url -O $path") == 0;
}

package main;

use Cwd 'abs_path';

sub main {
    my $brew_dir = abs_path($ENV{'NODEBREW_ROOT'} || $ENV{'HOME'} . '/.nodebrew');
    my $base_url = 'https://raw.githubusercontent.com/hokaccha/nodebrew/master/';
    my $nodebrew_url = "${base_url}nodebrew";
    my $bash_completion_url = "${base_url}completions/bash/nodebrew-completion";
    my $zsh_completion_url = "${base_url}completions/zsh/_nodebrew";
    my $fish_completion_url = "${base_url}completions/fish/nodebrew.fish";
    my $fetcher_type = `which curl` ? 'curl' :
                       `which wget` ? 'wget' :
                        die 'Need curl or wget';
    my ($command, $args) = Nodebrew::Utils::parse_args(@ARGV);

    Nodebrew->new(
        brew_dir        => $brew_dir,
        nodebrew_url    => $nodebrew_url,
        bash_completion_url => $bash_completion_url,
        zsh_completion_url  => $zsh_completion_url,
        fish_completion_url => $fish_completion_url,
        fetcher         => Nodebrew::Fetcher::get($fetcher_type),
        remote_list_url => {
            node => 'https://nodejs.org/dist/',
            iojs => 'https://iojs.org/download/#{release}/',
        },
        tarballs => {
            node => [
                "https://nodejs.org/dist/#{version}/node-#{version}.tar.gz",
                "https://nodejs.org/dist/node-#{version}.tar.gz",
            ],
            iojs => [
                "https://iojs.org/download/#{release}/#{version}/iojs-#{version}.tar.gz",
            ],
        },
        tarballs_binary => {
            node => [
                "https://nodejs.org/dist/#{version}/node-#{version}-#{platform}-#{arch}.tar.gz",
            ],
            iojs => [
                "https://iojs.org/download/#{release}/#{version}/iojs-#{version}-#{platform}-#{arch}.tar.gz",
            ],
        },
        iojs => 0,
    )->run($command, $args);
}

main() unless caller;