package Mojo::Alien::npm;
use Mojo::Base -base;

use Carp qw(croak);
use File::chdir;
use Mojo::File qw(path);
use Mojo::JSON qw(decode_json false);

use constant DEBUG => ($ENV{MOJO_NPM_DEBUG} || $ENV{MOJO_WEBPACK_DEBUG}) && 1;

has binary => sub {
  my $self = shift;
  return $ENV{MOJO_NPM_BINARY} ? $ENV{MOJO_NPM_BINARY} : 'npm';
};

has config => sub { path->to_abs->child('package.json') };
has mode   => sub { $ENV{NODE_ENV} || 'development' };

sub dependencies {
  my $self = shift;
  croak "Can't get dependency info without package.json" unless -r $self->config;

  my @args = $self->binary eq 'pnpm' ? qw(ls --json --silent) : qw(ls --json --parseable --silent);
  my $dependencies;

  eval {
    my $NPM = $self->_run(@args);

    # "WARN" might come from pnpm, and it also returns an array-ref
    $dependencies = decode_json(join '', grep { !/WARN/ } <$NPM>);
    $dependencies = $dependencies->[0] if ref $dependencies eq 'ARRAY';
    $dependencies = {map { %{$dependencies->{$_} || {}} } qw(devDependencies dependencies)};
  } or do {
    croak sprintf '%s failed: %s', join(' ', $self->binary, @args), $@;
  };

  my $package = decode_json $self->config->slurp;
  my %types   = (devDependencies => 'dev', dependencies => 'prod', optionalDependencies => 'optional');
  for my $type (qw(optionalDependencies devDependencies dependencies)) {
    for my $name (keys %{$package->{$type}}) {
      $dependencies->{$name}{required} = $package->{$type}{$name};
      $dependencies->{$name}{type}     = $types{$type};
      $dependencies->{$name}{version} //= '';
    }
  }

  return $dependencies;
}

sub init {
  my $self = shift;
  return $self if -r $self->config;
  $self->_run($self->binary eq 'pnpm' ? qw(init) : qw(init -y));
  croak "$self->{basename} init failed: @{[$self->config]} was not generated." unless -r $self->config;
  return $self;
}

sub install {
  my ($self, $name, $info) = @_;
  croak "Can't install packages without package.json" unless -w $self->config;

  # Make sure npm can install devDependencies and dependency
  local $self->{mode} = '';

  # Install everything
  do { $self->_run('install'); return $self } unless $name;

  # Install specific package
  $name = sprintf '%s@%s', $name, $info->{version} if $info->{version};
  my $type = sprintf '--save-%s', $info->{type} || 'dev';
  $self->_run('install', $name, $type);
  return $self;
}

sub _run {
  my $self = shift;
  my @cmd  = ($self->binary, @_);
  $self->{basename} ||= path($cmd[0])->basename;
  local $CWD = $self->config->dirname->to_string;
  local $ENV{NODE_ENV} = $self->mode;
  warn "[NPM] cd $CWD && @cmd\n" if DEBUG;
  open my $NPM, '-|', @cmd or die "Can't fork @cmd: $!";
  return $NPM if defined wantarray;
  map { DEBUG && print } <$NPM>;
}

# This is a utility function for the unit tests
sub _setup_working_directory {
  my ($class, $dir) = @_;
  my $remove_tree = $ENV{MOJO_NPM_CLEAN} ? 'remove_tree' : sub { };
  chdir(my $work_dir = path($dir ? $dir : ('local', path($0)->basename))->to_abs->tap($remove_tree)->make_path)
    or die "Couldn't set up working directory: $!";
  symlink $work_dir->dirname->child('node_modules')->make_path, 'node_modules'
    or warn "Couldn't set up shared node_modules: $!"
    unless -e 'node_modules';
  return $work_dir;
}

1;

=encoding utf8

=head1 NAME

Mojo::Alien::npm - Runs the external nodejs program npm

=head1 SYNOPSIS

  use Mojo::Alien::npm;
  my $npm = Mojo::Alien::npm->new;

  $npm->init;
  $npm->install;

=head1 DESCRIPTION

L<Mojo::Alien::webpack> is a class for runnig the external nodejs program
L<npm|https://npmjs.com/>.

=head1 ATTRIBUTES

=head2 binary

  $array_ref = $npm->binary;
  $npm = $npm->binary(['npm']);

The path to the npm executable. Default is "npm" unless the C<MOJO_NPM_BINARY>
environment variable has been set. This can also be set to "pnpm" in case you
prefer L<https://pnpm.io/>.

=head2 config

  $path = $npm->config;
  $npm = $npm->config(path->to_abs->child('package.json'));

Holds an I</absolute> path to "package.json".

=head2 mode

  $str = $npm->mode;
  $npm = $npm->mode('development');

Should be either "development" or "production". Will be used as "NODE_ENV"
environment variable when calling "npm".

=head1 METHODS

=head2 dependencies

  $dependencies = $npm->dependencies;

Used to get dependencies from L</config> combined with information from
C<npm ls>. The returned hash-ref looks like this:

  {
    "package-name" => {
      required => $str,  # version from package.json
      type     => $str,  # dev, optional or prod
      version  => $str,  # installed version
      ...
    },
    ...
  }

=head2 init

  $npm->init;

Used to create a default L</config> file.

=head2 install

  $npm->install;
  $npm->install('package-name');
  $npm->install('package-name', {type => 'prod', version => '0.1.2'});

Installs either all modules from L</config> or a given package by name. An
additional C<$info> hash can also be provided.

=head1 SEE ALSO

L<Mojolicious::Plugin::Webpack>.

=cut
