# A debhelper build system class for handling pkg-js-autopkgtest test.
#
# Copyright: 2019, Xavier Guimard <yadd@debian.org>
# License: GPL-2+

package Debian::Debhelper::Buildsystem::nodejs;

use strict;
use warnings;
use Debian::Debhelper::Dh_Lib
  qw(doit_noerror dpkg_architecture_value package_is_arch_all getpackages);
use File::Find;
use JSON;
use parent qw(Debian::Debhelper::Buildsystem);

sub DESCRIPTION {
    "pkg-js-tools test";
}

### CONSTANTS

# Regexp compiled from dh_nodejs/dh_files.excluded
my $fileIgnored =
qr/^(?:a(?:ut(?:otest\.watchr|hors?)|ppveyor\.yml)|c(?:o(?:ntribute|pying)|hange(?:log|s?))|.*\.(?:m(?:arkdown|d)|pdf|txt)|r(?:ollup\.config\.js|eadme)|t(?:sconfig\.json|ests?\.js)|(?:package-|yarn\.)lock|g(?:runt|ulp)file\.js|(?:license|\.).*|bower\.json|makefile|history)$/i;

# Regexp compiled from dh_nodejs/dh_dirs.excluded
my $dirIgnored = qr{^(?:(?:example|test|doc)s?|node_modules|\..*)(?:/.*)?}i;

# Extensions that overrides $fileIgnored if set in package.json#files
my $authorizedExt = qr/\.(?:js)$/;

### PRIVATE METHODS

sub open_file {
    my ( $self, $file ) = @_;
    die unless $file;
    my $f;
    open( $f, $file ) or die $!;
    my @lines = map {
        chomp;
        s/#.*$//;
        s/^\s*(.*?)\s*/$1/;
        $_ ? $_ : ();
    } <$f>;
    close $f;
    return @lines;
}

my %json;

# package.json cache
sub pjson {
    my ( $self, $dir ) = @_;
    unless ( -e "$dir/package.json" ) {
        print STDERR "/!\\ $dir/package.json not found\n";
        return undef;
    }
    return $json{$dir} if $json{$dir};
    my $pkgjson;
    open $pkgjson, "$dir/package.json";
    my $content = join '', <$pkgjson>;
    close $pkgjson;
    return $json{$dir} = JSON::from_json($content);
}

sub component_list {
    my $self = shift;
    my ( @components, %packages );
    if ( -e 'debian/watch' ) {
        map { push @components, $1 if (/component=([\w\-\.]+)/) }
          $self->open_file('debian/watch');
    }
    foreach my $component (@components) {
        if ( -d $component ) {
            my $package;
            eval { $package = $self->pjson($component)->{name} };
            print STDERR $@ if $@;
            next if ($@);
            $packages{$component} = $package;
        }
        else {
            print STDERR `ls -l`;
            die "Can't find $component directory in " . `pwd`;
        }
    }
    return \%packages;
}

sub pattern {
    my ( $self, $line ) = @_;
    my ( $p, $pattern );
    print "Parsing expression $line\n";
    $line =~ m/^(.*?)(\*.*)$/ or die "Bad line $line";
    my $tmp = $line;
    my ( $dir, $expr ) = ( $1, $2 );
    if ($dir) {
        $dir =~ s#/+$##;
        $p = $dir;
    }
    else {
        $p = '.';
    }
    my @dirs = ( "$p/", split( m#\*\*/#, $expr ) );
    $expr =~ s#.*\*\*/##;
    pop @dirs if ($expr);
    $expr = quotemeta($expr);
    $expr =~ s#\\\*#[^/]*#g;
    my $pat = '^' . join( '.*(?<=/)', @dirs ) . $expr . '$';
    print "Line $tmp becomes: $pat\n";
    eval { $pattern = qr/$pat/ };
    die << "EOF" if ($@);

Unable to parse expression: "$tmp" (converted into "$pat")
If it is correct, please fill a bug against pkg-js-tools.
To workaround, you can overwrite "files" field in debian/nodejs. See
/usr/share/doc/pkg-js-tools/README.md.

EOF
    return ( $p, $pattern );
}

# Private method to install modules: search for package.json#files field, if
# not found, install any files except ignored by regexp
sub install_module {
    my ( $self, $destdir, $archPath, $path, $dir, @excludedDirs ) = @_;
    my $re = (
        @excludedDirs
        ? '^(?:' . join( '|', @excludedDirs ) . ')(?:/.*)?$'
        : '##'
    );
    $re = qr/$re/;

    # If debian/nodejs/$dir/install or debian/nodejs/$dir/files or
    # "package.json#files" field exists, only its paths will be examined
    my @files;
    my $noFilesField = 1;
    my $skipFiles    = 1;
    if ( -e "debian/nodejs/$dir/install" ) {
        map {
            s/^\s*(.*?)\s*/$1/;
            next if /^\s*#/;
            my $dest = $path;
            my $src  = $_;
            if (/^(.+?)\s+(.+)$/) {
                $src = $1;
                my $tmp = $2;
                if ( $tmp =~ m{^/} ) {
                    $dest = "$destdir/$tmp";
                }
                else {
                    $dest = "$archPath/$tmp";
                }
            }

            #$self->doit_in_builddir( 'mkdir', '-p',  );
            $self->doit_in_builddir( 'mkdir', '-p', $dest )
              unless doit_noerror( { chdir => $self->get_buildpath },
                'test', '-e', $dest );
            $self->doit_in_builddir( 'cp', '--reflink=auto', '-a', "$dir/$src",
                $dest );
        } $self->open_file("debian/nodejs/$dir/install");
        return;
    }
    if ( -e "debian/nodejs/$dir/files" ) {
        @files =
          map {
            ( $_ and ( -e "$dir/$_" or /\*/ ) )
              ? "$dir/$_"
              : ( die "debian/nodejs/$dir/files: $_ does not exists" )
          } $self->open_file("debian/nodejs/$dir/files");
        push @files, "$dir/package.json"
          unless grep { $_ eq 'package.json' } @files;
        $noFilesField = 0;
        $skipFiles    = 0;
    }
    elsif ( $self->pjson($dir)->{files} ) {
        print qq{Found "files" field in $dir/package.json, using it\n};
        @files = map { ( -e "$dir/$_" or /\*/ ) ? "$dir/$_" : () }
          ( @{ $self->pjson($dir)->{files} } );
        push @files, "$dir/package.json"
          unless grep { $_ eq 'package.json' } @files;
        $noFilesField = 0;
    }
    else {
        print qq{No "files" field in $dir/package.json, install all files\n};
        @files = ($dir);
    }
    my @dest;
    foreach my $p (@files) {
        my $pattern;
        if ( $p =~ m/\*/ ) {
            ( $p, $pattern ) = $self->pattern($p);
        }
        find(
            sub {
                my $d = $File::Find::dir;
                return if $pattern and "$d/$_" !~ $pattern;
                $d =~ s#^$dir/?##;
                unless (
                    ( -d $_ )

                    # Don't parse components sub dir for main module
                    or ( $d and $d =~ $re and $skipFiles )

                    # Ignore our debian/ dir
                    or
                    ( $File::Find::dir =~ m{^(?:./)?debian} and $skipFiles )

                    # Ignore license, changelog, readme,... files
                    or (    $_ =~ $fileIgnored
                        and $noFilesField
                        and !$d )
                    or (    $_ =~ $fileIgnored
                        and $skipFiles
                        and !$d
                        and $_ !~ $authorizedExt )

                  # Ignore test directory unless specified in package.json#files
                    or (    $d
                        and $d =~ $dirIgnored
                        and $noFilesField )

                    # Ignore test.js file unless specified in package.json#files
                    or (    $File::Find::dir eq $dir
                        and /^(?:example|test)s?\.js$/i
                        and $noFilesField )

                    # Exclude any .eslint*
                    or ( /^\.eslint*/ and not $skipFiles )
                  )
                {
                    push @dest, [ $d, $File::Find::name ];
                }

                #else {
                #    print STDERR
                #      "$File::Find::name skipped\n $File::Find::dir\n $d\n";
                #    print STDERR "test1\n" if ( -d $_ );
                #    print STDERR "test2\n"
                #      if ( $d and $d =~ $re and $skipFiles );
                #    print STDERR "test3\n"
                #      if $File::Find::dir =~ m{^(?:./)?debian} and $skipFiles;
                #    print STDERR "test4\n"
                #      if $_ =~ $fileIgnored
                #      and $noFilesField
                #      and !$d;
                #    print STDERR "test5\n"
                #      if $skipFiles
                #      and !$d
                #      and $_ !~ $authorizedExt;
                #    print STDERR "test6\n"
                #      if $d
                #      and $d =~ $dirIgnored
                #      and $noFilesField;
                #    print STDERR "test7\n"
                #      if $File::Find::dir eq $dir
                #      and /^(?:example|test)s?\.js$/i
                #      and $noFilesField;
                #    print STDERR "test8\n" if /^\.eslint*/ and not $skipFiles;
                #}
            },
            $p
        );
    }
    foreach (@dest) {
        $self->doit_in_builddir( 'mkdir', '-p', "$path/$_->[0]" )
          unless doit_noerror( { chdir => $self->get_buildpath },
            'test', '-e', "$path/$_->[0]" );
        my $mode = ( -x $_->[1] ? '755' : '644' );
        $self->doit_in_builddir( 'cp', '--reflink=auto', '-a', $_->[1],
            "$path/$_->[0]/" );
    }
}

# PUBLIC METHODS
# --------------

sub new {
    my $class = shift;
    return $class->SUPER::new(@_);
}

sub check_auto_buildable {
    my $self = shift;
    if ( -e 'package.json' ) {
        return 1;
    }
    return 0;
}

# auto_configure step: if component are found, create node_modules links
sub configure {
    my $self       = shift;
    my $components = $self->component_list;
    my @dirs       = ( sort keys %$components );
    return unless @dirs;
    $self->doit_in_builddir( 'mkdir', 'node_modules' )
      unless -e 'node_modules';

    # Link each component in node_modules
    foreach my $component (@dirs) {
        my $package = $components->{$component};
        $self->doit_in_builddir( 'ln', '-s', "../$component",
            "node_modules/$package" )
          unless -e "debian/nodejs/$component/nolink";
    }

    # Links between components
    if ( -e 'debian/nodejs/component_links' ) {
        my %links = map {
            die "Malformed line $. in debian/nodejs/component_links: $_"
              unless /^([\w\-\.]+)\s+([\w\-\.]+)$/;
            my ( $src, $dest ) = ( $1, $2 );
            foreach my $s ( $src, $dest ) {
                die "component_links: Unknown component $s"
                  unless $components->{$s};
            }
            ( $src => $dest );
        } $self->open_file('debian/nodejs/component_links');
        foreach ( keys %links ) {
            $self->doit_in_builddir( 'mkdir', '-p', "$links{$_}/node_modules" )
              unless doit_noerror( { chdir => $self->get_buildpath },
                'test', '-e', "$links{$_}/node_modules" );
            $self->doit_in_builddir(
                'ln',       '-s',
                "../../$_", "$links{$_}/node_modules/$components->{$_}"
            );
        }
    }
}

# test step: simple require or real test if declared
sub test {
    my $self = shift;
    if ( -e 'debian/tests/pkg-js/test' ) {
        $self->doit_in_builddir( '/bin/sh', '-e', 'debian/tests/pkg-js/test' );
    }
    else {
        $self->doit_in_builddir( '/usr/bin/node', '-e', 'require(".")' );
    }
}

# install step:
#  - if no debian/install file found, install automatically module
#  - install components

sub install {
    my $self    = shift;
    my $destdir = shift;
    my @pkgs    = getpackages();
    my ($package) =
      $#pkgs ? ( grep { $_ =~ /^node-/ } getpackages() ) : @pkgs;
    ($package) = @pkgs unless ($package);
    my $archPath = "$destdir/"
      . (
        package_is_arch_all($package)
        ? '/usr/share/nodejs'
        : '/usr/lib/'
          . dpkg_architecture_value("DEB_HOST_GNU_TYPE")
          . '/nodejs'
      );
    my $node_module = $self->pjson('.')->{name};
    my $components  = $self->component_list;
    my @dirs        = ( sort keys %$components );

    # main install
    my $path = "$archPath/$node_module";
    $self->install_module( $destdir, $archPath, $path, '.', @dirs );

    # Component install
    return unless (@dirs);
    $path = "$path/node_modules";
    if ( -e 'debian/nodejs/submodules' ) {
        my @d = map { chomp; /^\w/ ? $_ : () }
          $self->open_file('debian/nodejs/submodules');
        unless (@d) {
            print
"Components auto-install is skipped by empty debian/nodejs/submodules";
            return;
        }
        my @tmp;
      CMP: foreach my $cmp (@d) {
            foreach my $dir (@dirs) {
                if ( $dir eq $cmp or $components->{$dir} eq $cmp ) {
                    push @tmp, $dir;
                    next CMP;
                }
            }
            die qq{In debian/nodejs/submodules, "$cmp" matches nothing};
        }
        @dirs = @tmp;
    }
    foreach my $dir (@dirs) {
        $self->install_module( $destdir, $archPath,
            "$path/$components->{$dir}", $dir );
    }
}

# clean step: try to clean node_modules directories
sub clean {
    my $self       = shift;
    my $components = $self->component_list;
    my @dirs       = ( sort keys %$components );
    if ( -e 'debian/nodejs/component_links' ) {
        my %links = map {
            die "Malformed line $. in debian/nodejs/component_links: $_"
              unless /^([\w\-\.]+)\s+([\w\-\.]+)$/;
            my ( $src, $dest ) = ( $1, $2 );
            unlink "$dest/node_modules/$components->{$src}";
            ( $src => $dest );
        } $self->open_file('debian/nodejs/component_links');
    }
    foreach my $component (@dirs) {
        my $package = $components->{$component};

        # Error not catched here
        unlink "node_modules/$package";

        # Will remove only empty directories
        rmdir "$component/node_modules";
    }
    rmdir 'node_modules';
}

1;
