#!/usr/bin/env perl
use strict;
use warnings;
use v5.20;

use List::Util   qw(first);
use AUR::Json    qw(parse_json_aur write_json);
use AUR::Query   qw(query_multi);
use AUR::Depends qw(recurse prune graph);
use AUR::Options qw(add_from_stdin);
my $argv0 = 'depends';

sub solve {
    my ($targets, $types, $callback, $opt_verify, $opt_provides, $opt_installed) = @_;
    
    # Retrieve AUR results (JSON -> dict -> extract depends -> repeat until none)
    my ($results, $pkgdeps, $pkgmap) = recurse($targets, $types, $callback);

    # Verify dependency requirements
    my ($dag, $dag_foreign) = graph($results, $pkgdeps, $pkgmap, $opt_verify, $opt_provides);
    my @removals = ();

    # Remove virtual dependencies from dependency graph (#1063)
    if ($opt_provides) {
        my @virtual = keys %{$pkgmap};

        # XXX: assumes <pkgmap> only contains keys with provides != pkgname
        @removals = prune($dag, \@virtual);
    }
    # Remove transitive dependencies for installed targets (#592)
    # XXX: prune from $dag_foreign as well?
    if (scalar @{$opt_installed}) {
        @removals = prune($dag, $opt_installed);
    }
    # Remove packages no longer in graph from results
    if (scalar @removals) {
        map { delete $results->{$_} } @removals;
    }
    # Return $dag for subsequent application of C<prune>
    return $results, $dag, $dag_foreign;
}

# tsv output for usage with aur-sync (aurutils <=10)
sub table_v10_compat {
    my ($results, $types) = @_;

    for my $pkg (values %{$results}) {
        next if not defined $pkg->{'PackageBase'};

        my ($name, $base, $version) = (
            $pkg->{'Name'}, $pkg->{'PackageBase'}, $pkg->{'Version'}
        );
        say join("\t", $name, $name, $base, $version, 'Self');

        for my $deptype (@{$types}) {
            my $depends = $pkg->{$deptype};
            next if (ref($depends) ne 'ARRAY');

            for my $dep (@{$depends}) {
                say join("\t", $name, $dep, $base, $version, $deptype);
            }
        }
    }
}

# tsv output for usage with aur-sync (aurutils >=11)
sub table {
    my $results = shift;

    for my $pkg (values %{$results}) {
        my ($name, $base, $version, $reqby) = (
            $pkg->{'Name'}, $pkg->{'PackageBase'}, $pkg->{'Version'}, $pkg->{'RequiredBy'}
        );

        for my $dep (keys %{$reqby}) {
            say join("\t", $name, $dep, $base // '-', $version // '-', $reqby->{$dep});
        }
    }
}

# package/dependency pairs for use with tsort(1) or aur-graph
# XXX: include optional column for versioned dependencies
sub pairs {
    my ($results, $key, $reverse) = @_;
    my %seen;  # filter out lines with equal pkgbase

    for my $pkg (values %{$results}) {
        my $target = $pkg->{$key};

        for my $reqby (keys %{$pkg->{'RequiredBy'}}) {
            my $rdep = $key eq 'Name' ? $reqby : $results->{$reqby}->{$key} // '-';
            my @pair = $reverse ? ($target, $rdep) : ($rdep, $target);

            say join("\t", @pair) if not defined $seen{($target, $rdep)};
            $seen{($target, $rdep)} = 1;
        }
    }
}

unless(caller) {
    # Command-line arguments
    use Getopt::Long;
    my $opt_depends      = 1;
    my $opt_makedepends  = 1;
    my $opt_checkdepends = 1;
    my $opt_optdepends   = 0;
    my $opt_mode         = "pairs";
    my $opt_pkgname      = 0;
    my $opt_show_all     = 0;  # implies $opt_pkgname = 1
    my $opt_reverse      = 0;
    my $opt_provides     = 1;
    my $opt_verify       = 0;
    my $opt_installed    = [];

    GetOptions(
        'ignore|assume-installed=s' => $opt_installed,
        'no-depends'      => sub { $opt_depends = 0 },
        'no-makedepends'  => sub { $opt_makedepends = 0 },
        'no-checkdepends' => sub { $opt_checkdepends = 0 },
        'optdepends'      => \$opt_optdepends,
        'no-provides'     => sub { $opt_provides = 0 },
        'v|verify'        => \$opt_verify,
        'n|pkgname'       => \$opt_pkgname,
        'b|pkgbase'       => sub { $opt_pkgname = 0 },
        'G|graph'         => sub { },  # noop
        't|table'         => sub { $opt_mode = "table" },
        'J|json'          => sub { $opt_mode = "json" },
        'jsonl'           => sub { $opt_mode = "jsonl" },
        'r|reverse'       => \$opt_reverse,
        'a|all|show-all'  => \$opt_show_all
    ) or exit(1);

    if (not scalar(@ARGV)) {
        say STDERR "$argv0: at least one argument required";
        exit(1);
    }

    # Handle '-' to take packages from stdin
    add_from_stdin(\@ARGV, ['-', '/dev/stdin']);

    # Exit gracefully on empty stdin, e.g. when piping from `aur repo -u`
    exit(0) if not scalar(@ARGV);

    # Variable dependency types (#826)
    my @types;
    push(@types, 'Depends')      if $opt_depends;
    push(@types, 'MakeDepends')  if $opt_makedepends;
    push(@types, 'CheckDepends') if $opt_checkdepends;
    push(@types, 'OptDepends')   if $opt_optdepends;

    # Array notation for `--assume-installed`
    @{$opt_installed} = map { split(',', $_) } @{$opt_installed};
    
    # Dependency handling
    sub callback_query {
        query_multi(terms => @_, type => 'info', callback => \&parse_json_aur)
    }

    # Retrieve AUR results
    #    JSON -> hash -> extract depends[] -> repeat until none -> prune
    my ($results, $dag, $dag_foreign) = solve(\@ARGV, \@types, \&callback_query,
        $opt_verify, $opt_provides, $opt_installed);

    # Add `RequiredBy` to results
    for my $name (keys %{$dag}) {
        $results->{$name}->{'RequiredBy'} = $dag->{$name};
    }
    # Add foreign (non-AUR) packages to results
    if ($opt_show_all) {
        for my $name (keys %{$dag_foreign}) {
            $results->{$name}->{'Name'} = $name;
            $results->{$name}->{'RequiredBy'} = $dag_foreign->{$name};
        }
    }
    # Format results
    if ($opt_mode eq 'pairs') {
        pairs($results, ($opt_pkgname or $opt_show_all) ? 'Name' : 'PackageBase', $opt_reverse);
    }
    elsif ($opt_mode eq 'table' and $opt_reverse) {
        table($results);
    }
    elsif ($opt_mode eq 'table') {
        table_v10_compat($results, \@types);
    }
    elsif ($opt_mode eq 'json') {
        say write_json($results);
    }
    elsif ($opt_mode eq 'jsonl') {
        map { say write_json $results->{$_} } keys %{$results};
    }
    else {
        say STDERR "$argv0: invalid mode selected";
        exit(1);
    }
}

# vim: set et sw=4 sts=4 ft=perl:
