#!/usr/bin/perl
#
# Copyright (C) 2010-2018 Trizen <echo dHJpemVuQHByb3Rvbm1haWwuY29tCg== | base64 -d>.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.
#

# Author: Daniel "Trizen" Șuteu
# License: GPLv3
# Created: 07 July 2010
# First rewrite: 16 February 2011
# Second rewrite: 24 March 2012
# Latest edit: 15 September 2018
# https://github.com/trizen/trizen

# Contributors:
#   notramo       - https://github.com/notramo
#   ccl2of4       - https://github.com/ccl2of4
#   nl6720        - https://github.com/nl6720
#   JJK96         - https://github.com/JJK96
#   nicman23      - https://github.com/nicman23
#   DragoonAethis - https://github.com/DragoonAethis

# Full list of contributors:
#   https://github.com/trizen/trizen/graphs/contributors

# Sincere thanks to everyone who helped and contributed in improving
# this program through pull-requests, feature-requests and bug-reports.

eval 'exec perl -S $0 ${1+"$@"}'
  if 0;    # not running under some shell

use 5.020;
use strict;
use warnings;

use experimental qw(signatures);

use List::Util qw(first any max);
use Encode qw(decode_utf8);
use Getopt::Std qw(getopts);
use URI::Escape qw(uri_escape_utf8);

use Term::ANSIColor qw(:constants);

use File::Path qw(make_path rmtree);
use File::Spec::Functions qw(catdir catfile tmpdir curdir rel2abs);

my $version  = '1.52';
my $execname = 'trizen';

my $AUR_V = '5';    # current version of AurJson

my $aur_base_url       = 'https://aur.archlinux.org';
my $aur_rpc_base_url   = "$aur_base_url/rpc.php";
my $aur_package_id_url = "$aur_base_url/packages.php?ID=%s";

# Home directory
my $home_dir = $ENV{HOME} || $ENV{LOGDIR} || (getpwuid $<)[7] || `echo -n ~`;

# Configuration directory
my $config_dir = catdir($ENV{XDG_CONFIG_HOME} || catdir($home_dir, '.config'), $execname);

# Configuration file
my $config_file = catfile($config_dir, "$execname.conf");

my $user = (getpwuid $<)[0] || substr(`/usr/bin/whoami`, 0, -1);
my $arch = substr(`/usr/bin/uname -m`, 0, -1);

my %ignored_pkgs;
if (-x '/usr/bin/pacconf') {
    %ignored_pkgs = map { ($_ => 1) } split(' ', `/usr/bin/pacconf IgnorePkg 2> /dev/null`);
}

# UTF-8 output
binmode(STDOUT, ':utf8');
binmode(STDERR, ':utf8');

# Prefer HTTPS v3
local $ENV{HTTPS_VERSION} = 3;

#----------------------- COLORS -----------------------#
my %c = (
         byellow => (BOLD YELLOW),
         bpurple => (BOLD MAGENTA),
         bblue   => (BOLD BLUE),
         bgreen  => (BOLD GREEN),
         bold    => (BOLD),
         bred    => (BOLD RED),
         bcyan   => (BOLD CYAN),
         reset   => (RESET),
        );

my $stdout_on_tty = -t STDOUT;
my $stdin_on_tty  = -t STDIN;

if (not $stdin_on_tty and any { $_ eq '-' } @ARGV) {
    push @ARGV, split(' ', join(' ', <STDIN>));
    close STDIN;
}

@ARGV = map { decode_utf8($_) } @ARGV;

# Disable colors
if (not $stdout_on_tty or (any { $_ eq '--nocolors' } @ARGV)) {
    %c = map { ($_ => "") } keys %c;
}

if (not -d $config_dir) {
    make_path($config_dir)
      or note(":: Unable to create directory <<$config_dir>>: {!$!!}");
}

#----------------------- GLOBAL VARIABLES -----------------------#
my %CONFIG = (
              show_comments                => 0,
              debug                        => 0,
              nocolors                     => 0,
              movepkg                      => 0,
              noedit                       => 0,
              noinstall                    => 0,
              nopull                       => 0,
              noinfo                       => 0,
              skipinteg                    => 0,
              force                        => 0,
              ask_for_retry                => 1,
              lwp_show_progress            => 0,
              lwp_env_proxy                => 1,
              lwp_timeout                  => 60,
              ssl_verify_hostname          => 1,
              packages_in_stats            => 5,
              git_clone_depth              => 0,
              split_packages               => 1,
              recompute_deps               => 1,
              pager_mode                   => 0,
              use_sudo                     => ((-x '/usr/bin/sudo') ? 1 : 0),
              su_command                   => '/usr/bin/su -c',
              sudo_command                 => '/usr/bin/sudo',
              sudo_autorepeat              => 0,
              sudo_autorepeat_at_runtime   => 0,
              sudo_autorepeat_interval     => 180,
              makepkg_command              => '/usr/bin/makepkg -scf',
              movepkg_dir                  => '/var/cache/pacman/pkg',
              pacman_local_dir             => '/var/lib/pacman/local',
              pacman_command               => '/usr/bin/pacman',
              show_diff_only               => 0,
              show_build_files_content     => 1,
              aur_results_sort_by          => 'name',
              aur_results_sort_order       => 'ascending',
              aur_results_votes            => 1,
              aur_results_popularity       => 1,
              aur_results_last_modified    => 1,
              aur_results_show_installed   => 1,
              flip_results                 => 0,
              flip_indices                 => 0,
              show_ood                     => 0,
              show_inexistent              => 1,
              show_unmaintained            => 1,
              install_built_with_Ud        => 0,
              install_built_with_noconfirm => 0,
              color_code_dependencies      => 0,
              syntax_highlighting          => ((-x '/usr/bin/highlight') ? 1 : 0),
              syntax_highlighting_cmd      => '/usr/bin/highlight -O ansi',
             );

$CONFIG{clone_dir} = catdir(tmpdir(), "$execname-$user");

my %lconfig = (
    %CONFIG,
    quiet         => 0,
    devel         => 0,
    needed        => 0,
    noconfirm     => 0,
    aur           => 0,
    search        => 0,                           # -s
    info          => 0,                           # -i
    clean         => 0,                           # -c
    local         => 0,                           # -l
    pkgbuild      => 0,
    print         => 0,
    list          => 0,
    sysupgrade    => 0,                           # -u
    comments      => 0,
    database      => 0,                           # -D
    files         => 0,                           # -F
    query         => 0,                           # -Q
    remove        => 0,                           # -R
    sync          => 0,                           # -S
    deptest       => 0,                           # -T
    upgrade       => 0,                           # -U
    get           => 0,
    regex         => 0,                           # -x
    refresh       => 0,                           # -y
    upgrades      => 0,                           # -u
    check         => 0,                           # -k
    with_deps     => 0,                           # -d
    maintainer    => 0,                           # -m
    nobuild       => 0,
    regular       => 0,                           # use only the regular repositories
    asdep         => 0,                           # alias for `--asdeps`
    asdeps        => 0,
    asexplicit    => 0,
    help          => 0,
    update_config => 0,
    ignore        => '',                          # packages to ignore during -Su
    as_root       => ($user eq 'root' ? 1 : 0),
    stats         => \&show_stats,
    version       => \&version,
              );

$lconfig{editor} = $ENV{VISUAL} || $ENV{EDITOR} || 'nano';
$lconfig{pager_command} = $ENV{PAGER} || 'less';

our $CONFIG;

sub dump_configuration ($config, $configuration_file) {

    my $config_header = <<"EOH";
#!/usr/bin/perl

# $execname v$version configuration file
# Updated on ${\(scalar localtime)}

EOH

    my $config_options_help = <<"EOT";
aur_results_last_modified    => bool    Show the date when the packages were last updated in AUR results.
aur_results_popularity       => bool    Show the popularity score in AUR results.
aur_results_show_installed   => bool    Show when a package is installed in AUR results.
aur_results_sort_by          => str     Sort the AUR results by "name", "votes", "popularity" or "date".
aur_results_sort_order       => str     Sort the AUR results in "ascending" or "descending" order.
aur_results_votes            => bool    Show the number of votes in AUR results.
ask_for_retry                => bool    When `makepkg` fails to build a package, offer the option for trying again.
clone_dir                    => str     Absolute path to the directory where to clone and build packages.
color_code_dependencies      => bool    Display the dependencies of a package in specific colors (green = installed; cyan = in repo; purple = in AUR).
debug                        => bool    Verbose mode.
flip_indices                 => bool    In search+install mode, show the indices of packages in reverse order.
flip_results                 => bool    Show the search results in reverse order.
force                        => bool    Pass the `--force` flag to `pacman`.
git_clone_depth              => int     Pass the `--depth int` flag to `git clone`. (0 means no limit)
install_built_with_Ud        => bool    Install built packages with `-Ud`, which skips dependency version checks.
install_built_with_noconfirm => bool    Install built packages with `--noconfirm`.
lwp_env_proxy                => bool    Use proxy settings defined in `env` (if any).
lwp_show_progress            => bool    Show the HTTPS requests made by LWP::UserAgent to the AUR servers.
lwp_timeout                  => int     Seconds after which an HTTPS connection is aborted.
makepkg_command              => str     The `makepkg` command that is used internally in building a package.
movepkg                      => bool    Move built packages in the directory `movepkg_dir`.
movepkg_dir                  => str     Absolute path to the directory where to move built packages (with `movepkg`).
nocolors                     => bool    Disable output colors for `$execname`.
noedit                       => bool    Do not prompt to edit files when installing an AUR package.
noinfo                       => bool    Do not display package information when installing an AUR package.
noinstall                    => bool    Do not install built packages -- builds only.
nopull                       => bool    Do not `git pull` new changes from the AUR git server.
packages_in_stats            => int     The number of packages to display in `--stats`
pacman_command               => str     The `pacman` command that is used internally for pacman operations.
pacman_local_dir             => str     Absolute path to the pacman's local directory.
pager_mode                   => bool    Show the build files in pager mode using pager.
recompute_deps               => bool    Recompute the dependencies of a package after its build files are inspected.
show_build_files_content     => bool    Show the content of the build files of a package before building it.
show_diff_only               => bool    When the build files of a package already exist locally, show the diff only.
show_comments                => bool    Show AUR comments for a package before building it.
show_inexistent              => bool    Warn about packages that do not exist in AUR, during -Su.
show_ood                     => bool    Warn about out-of-date marked packages, during -Su.
show_unmaintained            => bool    Warn about unmaintained packages, during -Su.
skipinteg                    => bool    Pass the `--skipinteg` argument to `makepkg`.
split_packages               => bool    Ask about installing the other parts of a split package.
ssl_verify_hostname          => bool    Ensure LWP::UserAgent connects to servers that have a valid certificate.
su_command                   => str     Command used when special permissions are required and `use_sudo` is set to 0.
sudo_autorepeat              => bool    Automatically repeat `sudo -v` in the background after a `sudo` command was first executed.
sudo_autorepeat_at_runtime   => bool    Execute `sudo -v` when `$execname` is first executed and apply the behavior of `sudo_autorepeat`.
sudo_autorepeat_interval     => int     Interval, in seconds, after which `sudo -v` is executed in background (with `sudo_autorepeat`).
sudo_command                 => str     Command used when special permissions are required and `use_sudo` is set to 1.
syntax_highlighting          => bool    Syntax hightling of the build files, using the `highlight` tool from [community].
syntax_highlighting_cmd      => str     The `highlight` command used in highlighting the syntax of the build files (with `syntax_highlighting`).
use_sudo                     => bool    Use the `sudo` command when special permissions are required.
EOT

    my %table;
    foreach my $line (split(/\n/, $config_options_help)) {
        if ($line =~ /^(\w+)\s*=>\s*(\w+)\s+(.+)/) {
            my ($name, $type, $desc) = ($1, $2, $3);

            $table{$name} = {
                             type => $type,
                             desc => $desc,
                            };
        }
    }

    open my $config_fh, '>:utf8', $configuration_file or return;

    require Data::Dump;
    my $dumped_config = q{our $CONFIG = } . Data::Dump::dump($config) . "\n";

    if ($home_dir eq $ENV{HOME}) {
        $dumped_config =~ s/\Q$home_dir\E/\$ENV{HOME}/g;
    }

    my $names_re = join('|', keys %table);
    my $max_width = max(map { length($_) } split(/\n/, $dumped_config));

    # Add description after each configuration option
    $dumped_config =~ s{
        ^\s*($names_re)(\s*=>\s*.*)
    }{
        sprintf("  %s %*s # %s", $1.$2, $max_width - length($1.$2), ' ',
            sprintf('%4s -- %s', $table{$1}{type}, $table{$1}{desc}))
    }gxme;

    print $config_fh $config_header, $dumped_config;
    close $config_fh;
}

if (-e $config_file and (-s _) > 1) {
    require $config_file;    # Load the configuration file

    if (ref($CONFIG) eq 'HASH') {
        my @valid_keys = grep { exists $CONFIG{$_} } keys(%$CONFIG);

        $CONFIG->{clone_dir} //= $CONFIG{clone_dir};

        if (join(' ', sort keys %CONFIG) ne join(' ', sort keys %$CONFIG)) {
            $lconfig{update_config} = 1;
        }

        @CONFIG{@valid_keys}  = @{$CONFIG}{@valid_keys};
        @lconfig{@valid_keys} = @{$CONFIG}{@valid_keys};
    }
    else {
        note(":: Invalid config file: {!" . ($! || '$CONFIG must be a HASH ref!') . '!}');
        $lconfig{update_config} = 1;
    }
}
else {
    msg(":: Saving configuration file...") if $lconfig{debug};
    dump_configuration(\%CONFIG, $config_file)
      or note(":: Cannot open file <<$config_file>> for write: {!$!!}");
}

if (not -d $lconfig{clone_dir}) {
    make_path($lconfig{clone_dir})
      or error(":: Unable to create clone directory <<$lconfig{clone_dir}>>: {!$!!}");
}

my $short_arguments = 'TDFCUNQRSGabcdefghiklmnopqrstuvwxy';

my @package_suffices = qw(
  .tar.7z
  .tar.Z
  .tar.bz2
  .tar.gz
  .tar.lrz
  .tar.lz
  .tar.lzma
  .tar.lzo
  .tar.xz
  .tgz
  .xz
  .7z
  .bz2
  .gz
  .lzma
  .lzo
  .tar
  .zip
  );

my %already_built;        # will keep track of built packages
my %already_installed;    # will keep track of installed packages
my %seen_package;         # will keep track of seen packages

my $pkg_suffices = join('|', map { quotemeta } @package_suffices);
my $pkg_suffix_re = qr/-[^-]+-\d+-\w+\.pkg(?:$pkg_suffices)\z/o;

my $sudo_autorepeat_pid;

sub exit_code ($code = $?) {

    if ($code >= 256) {
        $code >>= 8;
    }

    return $code;
}

# Main quit
sub main_quit ($code = undef) {
    if ($lconfig{update_config}) {
        dump_configuration(\%CONFIG, $config_file);
    }
    kill('TERM', $sudo_autorepeat_pid) if $sudo_autorepeat_pid;
    exit($code // exit_code());
}

#----------------------- USAGE -----------------------#
sub help {
    print <<"HELP";

========================== $c{bgreen}Trizen AUR Package Manager$c{reset} ==========================

$c{bold}usage:$c{reset} $execname [options] [pkgname] [pkgname] [...]

$c{bgreen}Main options:$c{reset}

    $c{bold}-S$c{reset}, $c{bold}--sync$c{reset}      : install packages (see: $execname -Sh)
    $c{bold}-C$c{reset}, $c{bold}--comments$c{reset}  : display AUR comments for a package
    $c{bold}-G$c{reset}, $c{bold}--get$c{reset}       : clones a package in the current directory
    $c{bold}-R$c{reset}, $c{bold}--remove$c{reset}    : remove packages from the system (see: pacman -Rh)
    $c{bold}-Q$c{reset}, $c{bold}--query$c{reset}     : query the package database (see: pacman -Qh)
    $c{bold}-F$c{reset}, $c{bold}--files$c{reset}     : query the files database (see: pacman -Fh)
    $c{bold}-D$c{reset}, $c{bold}--database$c{reset}  : operate on the package database (see: pacman -Dh)
    $c{bold}-T$c{reset}, $c{bold}--deptest$c{reset}   : check dependencies (see: pacman -Th)
    $c{bold}-U$c{reset}, $c{bold}--upgrade$c{reset}   : install built packages from $lconfig{clone_dir} or `pwd`

$c{bgreen}Other options:$c{reset}

    $c{bold}-q$c{reset}, $c{bold}--quiet$c{reset}     : do not display any warnings
    $c{bold}-r$c{reset}, $c{bold}--regular$c{reset}   : use only the regular repositories
        $c{bold}--stats$c{reset}     : show stats about the installed packages
        $c{bold}--nocolors$c{reset}  : disable text colors
        $c{bold}--debug$c{reset}     : activate the debug/verbose mode
        $c{bold}--help$c{reset}      : print this message and exit
        $c{bold}--version$c{reset}   : print version and exit

$c{bgreen}See also:$c{reset}

    $c{bold}$execname -Sh$c{reset}
    $c{bold}$execname -Gh$c{reset}

$c{bred}::$c{reset} Each configuration key is a valid option when preceded with '--'
$c{bred}::$c{reset} Configuration file: $c{bold}$config_file$c{reset}
HELP
    main_quit();
}

sub sync_help {
    print <<"SYNC_HELP";
$c{bold}usage:$c{reset} $execname {-S --sync} [options] [package(s)]

$c{bgreen}Main options:$c{reset}

  $c{bold}-s$c{reset}, $c{bold}--search$c{reset}        : search for packages
  $c{bold}-i$c{reset}, $c{bold}--info$c{reset}          : show info for packages
  $c{bold}-m$c{reset}, $c{bold}--maintainer$c{reset}    : show packages maintained by <username>
  $c{bold}-p$c{reset}, $c{bold}--pkgbuild$c{reset}      : show PKGBUILD only
  $c{bold}-l$c{reset}, $c{bold}--local$c{reset}         : build and install packages from `pwd`
  $c{bold}-u$c{reset}, $c{bold}--sysupgrade$c{reset}    : upgrade installed packages
  $c{bold}-y$c{reset}, $c{bold}--refresh$c{reset}       : refresh package databases (with: -u)
  $c{bold}-c$c{reset}, $c{bold}--clean$c{reset}         : clean the cache directory of `$execname` and `pacman`
  $c{bold}-a$c{reset}, $c{bold}--aur$c{reset}           : only AUR operations (with: -c, -u, -s, -i)

$c{bgreen}Other options:$c{reset}

      $c{bold}--devel$c{reset}         : update VCS packages during -Su
      $c{bold}--show-ood$c{reset}      : show out-of-date flagged packages during -Su
      $c{bold}--noinfo$c{reset}        : do not display package info after cloning
      $c{bold}--nopull$c{reset}        : do not `git pull` new changes
      $c{bold}--noedit$c{reset}        : do not prompt to edit files
      $c{bold}--nobuild$c{reset}       : do not build packages (implies --noedit)
      $c{bold}--noinstall$c{reset}     : do not install packages after building
      $c{bold}--needed$c{reset}        : do not reinstall up-to-date packages
      $c{bold}--asdeps$c{reset}        : install packages as non-explicitly installed
      $c{bold}--asexplicit$c{reset}    : install packages as explicitly installed
      $c{bold}--force$c{reset}         : pass the `--force` argument to `pacman`
      $c{bold}--skipinteg$c{reset}     : pass the `--skipinteg` argument to `makepkg`
      $c{bold}--noconfirm$c{reset}     : do not ask for any confirmation
      $c{bold}--movepkg$c{reset}       : move built packages into pacman's cache directory
      $c{bold}--movepkg-dir=s$c{reset} : move built packages in this directory (implies --movepkg)
      $c{bold}--clone-dir=s$c{reset}   : directory where to clone and build packages
      $c{bold}--editor=s$c{reset}      : editor command used to edit build files
      $c{bold}--ignore=s$c{reset}      : space-separated list of packages to ignore during -Su

$c{bgreen}Examples:$c{reset}

    $c{bold}$execname -S  <package>$c{reset}     # install <package>
    $c{bold}$execname -Ss <keyword>$c{reset}     # search for <keyword>
    $c{bold}$execname -Si <package>$c{reset}     # show info about <package>

SYNC_HELP

    main_quit();
}

sub get_help {
    print <<"GET_HELP";
$c{bold}usage:$c{reset} $execname {-G --get} [options] [package(s)]

$c{bgreen}Main options:$c{reset}

    $c{bold}-d$c{reset}, $c{bold}--with-deps$c{reset}     : clones a package with all needed AUR dependencies

$c{bgreen}Examples:$c{reset}

    $c{bold}$execname -G  <package>$c{reset}     # clones <package>
    $c{bold}$execname -Gd <package>$c{reset}     # clones <package> along with its AUR dependencies

GET_HELP

    main_quit();
}

@ARGV or help();

sub version {
    say "$execname $version";
    main_quit();
}

#----------------------- PARSING ARGUMENTS -----------------------#
sub parse_long_arguments (@args) {
    Getopt::Long::GetOptionsFromArray(
        \@args,
        \%lconfig,
        map {
                (defined($lconfig{$_}) and $lconfig{$_} =~ /^[01]\z/) ? $_
              : ref($lconfig{$_}) ? $_
              : "$_=s"
        } keys %lconfig,
    );
}

sub parse_short_arguments () {
    getopts($short_arguments, \%lconfig);
}

my @keyword_arguments;
my @leftover_short_arguments;
my @leftover_long_arguments;

sub parse_arguments (@arguments) {

    my @long_arguments;

    foreach my $arg (@arguments) {

        if ($arg =~ /^--(\w.*?)=(.+)/) {
            my ($option, $value) = ($1, $2);

            $option =~ tr/-/_/;

            if (exists $lconfig{$option}) {
                $lconfig{$option} = $value;

                # --movepkg_dir implies --movepkg
                if ($option eq 'movepkg_dir') {
                    $lconfig{movepkg} = 1;
                }

                next;
            }
        }

        if ($arg =~ /^--(\w.*)/) {
            my $option = ($1 =~ tr/-/_/r);

            if (exists $lconfig{$option}) {
                push @long_arguments, "--$option";
                next;
            }
        }

        next if $arg =~ /^--?\z/;    # ignore '--' and '-'

        if ($arg =~ /^--/) {
            push @leftover_long_arguments, $arg;
        }
        elsif ($arg =~ /^-/) {
            $arg =~ tr/r//d;
            next if $arg eq '-';
            push @leftover_short_arguments, $arg;
        }
        else {
            push @keyword_arguments, $arg;
        }
    }

    if (@long_arguments) {
        require Getopt::Long;
        Getopt::Long::Configure('no_ignore_case');
        parse_long_arguments(@long_arguments);
    }

    my @short_arguments = grep { /^-[$short_arguments]/ } @ARGV;

    if (@short_arguments) {
        local @ARGV = @short_arguments;
        parse_short_arguments();
    }
}

parse_arguments(@ARGV);

if ($lconfig{as_root}) {
    warn "\n\t\t$c{bred}!!! You are running $c{bgreen}${execname}$c{reset}$c{bred} as root !!!$c{reset}\n\n" if $lconfig{S};
}

if ($lconfig{ignore}) {
    @ignored_pkgs{split(/[,\s]+/, $lconfig{ignore})} = ();
}

if ($lconfig{skipinteg}) {
    $lconfig{makepkg_command} .= ' --skipinteg';
}

if ($lconfig{noconfirm}) {
    $Term::UI::AUTOREPLY = 1;
    $lconfig{makepkg_command} .= ' --noconfirm';
}
else {
    $Term::UI::AUTOREPLY = 0;
}

# `--asdep` as alias for `--asdeps`
if ($lconfig{asdep}) {
    $lconfig{asdeps} = delete $lconfig{asdep};
}

# `-q` as alias for `--quiet`
if ($lconfig{q}) {
    $lconfig{quiet} = delete $lconfig{q};
}

# `-a` as alias for `--aur`
if ($lconfig{a}) {
    $lconfig{aur} = delete $lconfig{a};
}

if ($lconfig{debug}) {
    $lconfig{lwp_show_progress} = 1;
}

if ($lconfig{quiet}) {
    $SIG{__WARN__} = sub { };
}

if ($lconfig{nocolors}) {
    %c = map { $_ => q{} } keys %c;
}

if ($lconfig{movepkg}) {
    $lconfig{movepkg_dir} = rel2abs($lconfig{movepkg_dir});
}

if ($lconfig{nobuild}) {
    $lconfig{noedit}         = 1;
    $lconfig{recompute_deps} = 0;
}

$lconfig{clone_dir} = rel2abs($lconfig{clone_dir});

if (not -d $lconfig{clone_dir}) {
    make_path($lconfig{clone_dir})
      or error(":: Unable to create directory <<$lconfig{clone_dir}>>: {!$!!}");
}

if (    $lconfig{sudo_autorepeat_at_runtime}
    and $lconfig{use_sudo}
    and !$lconfig{as_root}) {
    start_sudo_autorepeat();
}

#----------------------- WORK AREA -----------------------#

# Run-time loaded modules
require Term::UI;
require Term::ReadLine;
require LWP::UserAgent;
require HTTP::Message;

# Initializing module objects
my $term = Term::ReadLine->new("$execname $version");

my $lwp = LWP::UserAgent->new(
                              env_proxy     => $lconfig{lwp_env_proxy},
                              show_progress => $lconfig{lwp_show_progress},
                              timeout       => $lconfig{lwp_timeout},
                              ssl_opts      => {verify_hostname => $lconfig{ssl_verify_hostname}, SSL_version => 'TLSv1_2'},
                              agent         => "Mozilla/5.0 (CLI; U; gzip; en-US) $execname/$version",
                             );

sub format_line ($string, $color) {

    if ($string =~ /^:: (.*)/) {

        my $substr  = $1;
        my $new_str = $color . '::' . $c{reset} . ' ';

        $substr =~ s/<<(.*?)>>/$c{bgreen}$c{bold}$1$c{reset}/gs;
        $substr =~ s/\{!(.*?)!\}/$c{bred}$c{bold}$1$c{reset}/gs;

        if ($substr =~ /^(.+?: )(.+)/) {
            $substr = $1 . $c{bgreen} . $2;
        }

        $new_str .= $substr;
        $new_str .= $c{reset};

        return $new_str;
    }

    return $string;
}

sub msg ($line) {
    say format_line($line, $c{bblue});
}

sub note ($line) {
    warn(format_line($line, $c{bred}) . "\n");
}

sub error ($line) {
    die format_line($line, $c{bred});
}

sub json2perl ($json) {
    require JSON;
    JSON::from_json($json);
}

sub start_sudo_autorepeat () {
    if (!$sudo_autorepeat_pid) {

        system($lconfig{sudo_command}, '-v');

        my $parent_pid = $$;
        $sudo_autorepeat_pid = fork;

        # Child process
        if ($sudo_autorepeat_pid == 0) {
            while ($parent_pid == getppid) {
                system($lconfig{sudo_command}, '-v');
                sleep $lconfig{sudo_autorepeat_interval};
            }
            exit;
        }
    }
}

sub get ($url) {

    # Don't connect to the AUR servers in `--regular` mode
    if ($lconfig{regular} or $lconfig{r}) {
        return;
    }

    state $accepted_encodings = HTTP::Message::decodable();

    my $response = $lwp->get($url, 'Accept-Encoding' => $accepted_encodings);

    if ($response->is_success) {
        my $content = $response->decoded_content;

        if (not defined $content) {
            note(":: Unable to decode HTML content: $@");
            return;
        }

        return $content;
    }
    else {
        note(":: Unable to GET $url ==> " . $response->status_line);
    }

    return;
}

sub get_comments ($id) {

    $id // return;
    $id =~ /^\d+\z/ or return;

    my $url = sprintf($aur_package_id_url, $id);
    my $content = get($url) // return;

    require HTML::Entities;

    my @comments;
    while (
        $content =~ m{
            <h4\b.*?>\s*
                (\S(?-s:.*?))\ commented\ on\ (\d\d\d\d-\d\d-\d\d\ \d\d:\d\d)
             \s*</h4>\s*
            <div\b.*?class="article-content">\s*
                <div> \s* (.*?) </div>\s*
            </div>
        }gsix
      ) {

        my $author  = $1;
        my $date    = $2;
        my $comment = $3;

        $comment =~ s{<.*?>}{}gs;
        $author =~ s{<.*?>}{}gs;
        $comment = HTML::Entities::decode_entities($comment);

        #$comment =~ s/\h{2,}/ /g;
        $comment = strip_space($comment);

        unshift @comments, <<"EOC";
$c{bold}Comment by $c{bgreen}$author$c{reset}$c{bold} on $date$c{reset}
$comment
EOC
    }
    return @comments;
}

sub get_pacman_arguments ($type, @options) {

    my @args;

    if ($type eq 'short' or $type eq 'all') {
        push @args, @leftover_short_arguments;
    }

    if ($type eq 'long' or $type eq 'all') {
        push @args, @leftover_long_arguments;
    }

    foreach my $option (@options) {
        push(@args, '--' . $option) if $lconfig{$option};
    }

    return @args;
}

sub execute_pacman_command ($needs_root, @cmd) {

    my $pacman_command = join(
        q{ },
        do {
            my %seen;
            ($lconfig{pacman_command}, grep { !$seen{$_}++ } map { quotemeta($_) } @cmd);
          }
    );

    if (    $needs_root
        and $lconfig{sudo_autorepeat}
        and $lconfig{use_sudo}
        and !$lconfig{as_root}) {
        start_sudo_autorepeat();
    }

    my $user_pacman_command = (
                                 $needs_root
                               ? $lconfig{use_sudo}
                                     ? "$lconfig{sudo_command} $pacman_command"
                                     : qq{$lconfig{su_command} "$pacman_command"}
                               : $pacman_command
                              );

    msg(":: Pacman command: " . ($user_pacman_command =~ s/\\(.)/$1/gr)) if $lconfig{debug};

    {
        system($lconfig{as_root} ? $pacman_command : $user_pacman_command);

        if ($? and $needs_root) {

            if (not $lconfig{ask_for_retry}) {
                main_quit();
            }

            note(":: Exit code: " . exit_code()) if $lconfig{debug};
            $term->ask_yn(prompt => "$c{bold}=>> Try again?$c{reset}", default => 'n') and redo;
        }
    }

    return $? ? 0 : 1;
}

sub is_available_in_pacman_repo ($pkgname) {

    my $output = `$lconfig{pacman_command} -Sp \Q$pkgname\E > /dev/stdout 2>&1`;

    # In repo if the output has at least two lines
    if ($output =~ /.\R:: /) {
        return 1;
    }

    my $exit_code = $?;
    $? = 0;
    return ($exit_code ? 0 : 1);
}

sub package_is_installed ($pkgname, $strict = 1) {

    my $installed_packages = {};

    if (!$strict and ref($lconfig{_installed_packages_cache}) eq 'HASH') {
        $installed_packages = $lconfig{_installed_packages_cache};
    }
    else {

        if (opendir(my $dir_h, $lconfig{pacman_local_dir})) {

            # Performance optimization when $strict is true
            if ($strict) {

                my $dirs_string = '/' . join('/', readdir($dir_h)) . '/';
                closedir($dir_h);

                if ($dirs_string =~ m{/\Q$pkgname\E-([^-]+-\d+)/}i) {
                    return $1;
                }

                return;
            }

            foreach my $dir (readdir($dir_h)) {
                if (scalar(reverse($dir)) =~ /^(.+?-.+?)-(.+)/) {

                    my $name    = reverse($2);
                    my $version = reverse($1);

                    $installed_packages->{$name} = $version;
                }
            }

            closedir($dir_h);
        }

        if (not keys(%$installed_packages)) {
            %$installed_packages = map { split(' ', $_) } `$lconfig{pacman_command} -Q`;
        }

        # Cache the installed packages
        $lconfig{_installed_packages_cache} = $installed_packages;
    }

    if (exists $installed_packages->{$pkgname}) {
        return $installed_packages->{$pkgname};
    }

    return;
}

sub package_is_provided ($pkgname) {
    chomp(my $deptest = `$lconfig{pacman_command} -T \Q$pkgname\E`);
    return ($deptest eq '');
}

sub versioncmp ($v1, $v2) {
    `/usr/bin/vercmp \Q$v1\E \Q$v2\E` + 0;
}

sub strip_space ($string) {
    $string // return '';
    $string =~ s/^\s+//;
    return unpack 'A*', $string;
}

sub strip_version ($version) {
    $version =~ s/\s*[<=>]=?.+//sg;
    return strip_space($version);
}

sub parse_package_name_and_version ($pkgname) {

    if ($pkgname =~ /^(.*?)([<=>]=?)(.*)/) {
        return ($1, $2, $3);
    }

    return ($pkgname, undef, undef);
}

sub info_for_package ($pkgname) {

    my $info = get_rpc_info($pkgname) // return;

    if (ref($info->{results}) ne 'ARRAY') {
        note(":: Unable to get info for package: $pkgname") if $lconfig{debug};
        return;
    }

    $info->{resultcount} > 0 or return;

    if ($info->{resultcount} > 1) {
        msg(":: Found <<$info->{resultcount}>> packages.");
        print "\n";
        my @packages = map { $_->{Name} } @{$info->{results}};
        my %table = (map { $packages[$_] => $_ } 0 .. $#packages);
        my $reply = $term->get_reply(
                                     print_me => "\n$c{bold}=>> Select a package$c{reset}",
                                     prompt   => "$c{bold}Select$c{reset}",
                                     choices  => \@packages,
                                     default  => $packages[0],
                                    );
        $info = $info->{results}[$table{$reply}];
    }
    else {
        $info = $info->{results}[0];
    }

    return $info;
}

sub download_package ($pkgname, $path) {

    my $info = info_for_package($pkgname) // return;

    my $pkg_base = $info->{PackageBase};
    my $git_url  = "$aur_base_url/$pkg_base.git";
    my $dir_name = rel2abs(catdir($path, $pkg_base));

    if ((-d $dir_name) and (-d catdir($dir_name, '.git'))) {

        chomp(my $old_commit_hash = `/usr/bin/git -C \Q$dir_name\E rev-parse HEAD`);

        if (!$lconfig{nopull}) {
#<<<
            system('/usr/bin/git', '-C', $dir_name, 'reset', '--hard', 'HEAD', ($lconfig{quiet} ? ('-q') : ())) && return;
            system('/usr/bin/git', '-C', $dir_name, 'pull',  '--ff',           ($lconfig{quiet} ? ('-q') : ())) && return;
#>>>
        }

        $info->{_exists_in_cache} = 1;

#<<<
        print decode_utf8(scalar `/usr/bin/git -C \Q$dir_name\E diff --no-ext-diff $old_commit_hash --color=always ':!.SRCINFO'`);
#>>>
    }
    else {

        # Clone the build files
        system('/usr/bin/git', '-C', $path, 'clone',
               ($lconfig{git_clone_depth} > 0 ? "--depth=$lconfig{git_clone_depth}" : ()),
               ($lconfig{quiet} ? ('-q') : ()), $git_url)
          && return;
    }

    if ($lconfig{debug}) {
        msg(":: Changing directory to: $path");
    }

    chdir($path) or do {
        note(":: Unable to chdir() to <<$path>>: {!$!!}");
        return;
    };

    if ($lconfig{debug}) {
        msg(":: Trying to change directory to: $dir_name");
    }

    $info->{_localpath} = $dir_name;    # directory that contains the PKGBUILD

    if (-d $dir_name) {

        chdir($dir_name) or do {
            note(":: Unable to chdir() to <<$dir_name>>: {!$!!}");
            return;
        };

        msg(":: Changed directory successfully to: $dir_name") if $lconfig{debug};
    }

    return $info;
}

sub get_rpc_info ($pkgname) {
    json2perl(get("$aur_rpc_base_url?v=$AUR_V&type=info&arg=" . uri_escape_utf8($pkgname)) // return);
}

sub indent_array ($elems) {
    my $first = shift(@$elems) // 'None';

    @$elems or return $first;

    my $rest = join("\n", @$elems);
    $rest =~ s/^\s+//gm;
    $rest =~ s/^/' ' x 18/gem;

    return "$first\n$rest";
}

sub color_code_dependencies ($deps) {

    $lconfig{'color_code_dependencies'} || return $deps;

    my @aur_packages;
    my @repo_packages;
    my @installed_packages;

    foreach my $pkgname (@$deps) {
        if (package_is_provided($pkgname)) {
            push @installed_packages, "$c{bgreen}$pkgname$c{reset}";
        }
        elsif (is_available_in_pacman_repo($pkgname)) {
            push @repo_packages, "$c{bcyan}$pkgname$c{reset}";
        }
        else {
            push @aur_packages, "$c{bpurple}$pkgname$c{reset}";
        }
    }

    return [@installed_packages, @repo_packages, @aur_packages];
}

sub show_info ($info) {

    say map { sprintf $c{bold} . $_->[0], $c{reset} . $_->[1] }
      ["Repository      : %s\n", "$c{bpurple}AUR$c{reset}"],
      ["Name            : %s\n", "$c{bold}$info->{Name}$c{reset}"],
      ["Version         : %s\n", $info->{Version} // 'Unknown'],
      ["Maintainer      : %s\n", $info->{Maintainer} // "$c{bred}None$c{reset}"],
      ["URL             : %s\n", $info->{URL}],
      ["AUR URL         : %s\n", sprintf($aur_package_id_url, $info->{ID})],
      ["License         : %s\n", indent_array($info->{License} // [])],
      ["Votes           : %s\n", $info->{NumVotes}],
      ["Popularity      : %s\n", sprintf("%.2g%%", $info->{Popularity})],
      ["Installed       : %s\n", package_is_installed($info->{Name}) ? "$c{bgreen}Yes$c{reset}" : 'No'],
      ["Out Of Date     : %s\n", $info->{OutOfDate} ? "$c{bred}Yes$c{reset}" : 'No'],
      ["Depends On      : %s\n", indent_array(color_code_dependencies($info->{Depends} //      []))],
      ["Make Deps       : %s\n", indent_array(color_code_dependencies($info->{MakeDepends} //  []))],
      ["Check Deps      : %s\n", indent_array(color_code_dependencies($info->{CheckDepends} // []))],
      ["Optional Deps   : %s\n", indent_array(color_code_dependencies($info->{OptDepends} //   []))],
      ["Provides        : %s\n", indent_array($info->{Provides} //  [])],
      ["Conflicts With  : %s\n", indent_array($info->{Conflicts} // [])],
      ["Replaces        : %s\n", indent_array($info->{Replaces} //  [])],
      ["Package Base    : %s\n", $info->{PackageBase}],
      ["Last Update     : %s\n", scalar localtime($info->{LastModified} || $info->{FirstSubmitted})],
      ["Description     : %s\n", $info->{Description}];

    return 1;
}

sub find_local_package ($pkgname, $dir) {

    my $newest_package;

    opendir(my $dir_h, $dir)
      or do {
        note(":: Cannot access directory <<$dir>>: {!$!!}");
        return;
      };

    while (defined(my $file = readdir $dir_h)) {
        if ($file =~ m{^\Q$pkgname\E$pkg_suffix_re}i) {

            # When there exists more than one built packages, get the latest one built
            if (defined $newest_package) {
                if ((-M "$dir/$file") < (-M $newest_package)) {
                    $newest_package = "$dir/$file";
                }
            }

            # When $newest_package is undefined, assign the first package found
            else {
                $newest_package = "$dir/$file";
            }
        }
    }

    closedir $dir_h;
    return $newest_package;
}

sub move_built_package ($tarball) {

    if (not -d $lconfig{movepkg_dir}) {
        make_path($lconfig{movepkg_dir})
          or note(":: Unable to create $lconfig{movepkg_dir}: {!$!!}");
    }
    if (-d $lconfig{movepkg_dir}) {
        if (-w _) {

            require File::Copy;
            require File::Basename;

            msg(":: Moving <<$tarball>> into <<$lconfig{movepkg_dir}>>") if $lconfig{debug};
            File::Copy::move($tarball, catfile($lconfig{movepkg_dir}, File::Basename::basename($tarball)))
              or note(":: Unable to move <<$tarball>> into <<$lconfig{movepkg_dir}>>: {!$!!}");
        }
        else {
            msg(":: Moving <<${tarball}>> into <<$lconfig{movepkg_dir}>>");

            system($lconfig{use_sudo}
                   ? "$lconfig{sudo_command} mv '${tarball}' '$lconfig{movepkg_dir}'"
                   : qq{$lconfig{su_command} 'mv \Q$tarball\E \Q$lconfig{movepkg_dir}\E'}
                  );

            if ($?) {
                note(":: Unable to move <<$tarball>> into <<$lconfig{movepkg_dir}>> -- exit code: " . exit_code());
                if ($term->ask_yn(prompt => "$c{bold}=>> Try again?$c{reset}", default => 'n')) {
                    move_built_package($tarball);
                }
            }
            else {
                msg(":: <<$tarball>> has been successfully moved into <<$lconfig{movepkg_dir}>>.");
            }
        }
    }
}

sub recompute_dependencies ($info) {

    msg(":: Recomputing dependencies...") if $lconfig{debug};

    my @srcinfo = map { decode_utf8($_) } `/usr/bin/makepkg --printsrcinfo`;

    if ($?) {
        note(":: `makepkg --printsrcinfo` exited with code: " . exit_code());
        return;
    }

    my %data;
    my $current_pkgname = $info->{Name};

    foreach my $line (@srcinfo) {
        if ($line =~ /^\s*(\w+)\s*=\s*(.*\S)/) {
            my ($key, $value) = ($1, $2);

            # Build the package (again)
            if ($key eq 'pkgname') {
                $already_built{lc $value} = 0;
                $current_pkgname = $value;
            }

            if ($current_pkgname eq $info->{Name}) {
                push @{$data{$key}}, $value;
            }
        }
    }

    my %pairs = (
                 Depends      => 'depends',
                 License      => 'license',
                 MakeDepends  => 'makedepends',
                 OptDepends   => 'optdepends',
                 Provides     => 'provides',
                 Conflicts    => 'conflicts',
                 CheckDepends => 'checkdepends',
                );

    while (my ($key1, $key2) = each %pairs) {
        $info->{$key1} = $data{$key2};
    }

    return 1;
}

sub output_file_content ($file) {

    if ($lconfig{pager_mode}) {
        print "\n";
        system("$lconfig{pager_command} \Q$file\E");
        return 1;
    }

    say "\n", '-' x 80;
    msg(":: Content of <<$file>>");
    say '-' x 80, "\n";

    # Syntax highlighting of the build files, using the `highlight` tool
    if ($lconfig{syntax_highlighting}) {
        require File::Basename;

        my $syntax = 'text';
        if (File::Basename::basename($file) eq 'PKGBUILD' or $file =~ /\.(?:install|sh)\z/) {
            $syntax = 'bash';
        }
        elsif ($file =~ /\.(\w+)\z/) {
            $syntax = $1;
        }

        system(join(' ', $lconfig{syntax_highlighting_cmd}, "--syntax=$syntax", '--force', quotemeta($file)));

        say '';
        return 1;
    }

    open my $fh, '<:utf8', $file or do {
        note(":: Unable to open <<$file>> for reading: {!$!!}");
        return;
    };

    eval {
        local $/ = undef;
        my $content = <$fh>;
        $content = reverse($content);
        $content =~ s/^\s+//;
        say scalar reverse($content) . "\n";
    };

    $@ && return;

    close $fh;
    return 1;
}

sub show_and_edit_text_files ($info) {

    if (!$lconfig{show_build_files_content}) {
        print "\n";
    }

    require File::Basename;
    chomp(my @files = map { File::Basename::basename(decode_utf8($_)) } `/usr/bin/git ls-files`);

    foreach my $file ('PKGBUILD', grep { $_ ne 'PKGBUILD' and -f and not -z _ } @files) {

        next if substr($file, -1) eq q{~};       # ignore backup (~) files
        next if substr($file, -4) eq q{.bak};    # ignore backup (.bak) files
        next if substr($file, 0, 1) eq q{.};     # ignore hidden files

        my $abs_file = rel2abs($file);

        if ($lconfig{show_build_files_content}) {
            if ($lconfig{show_diff_only} and $info->{_exists_in_cache}) {
                print "\n";
            }
            elsif (-T $abs_file) {               # output when it's a text-file
                output_file_content($abs_file);
            }
        }

        if (!$lconfig{noedit}) {
#<<<
            my $edit = $term->ask_yn(
                prompt  => "$c{bold}=>> Edit $file?$c{reset}",
                default => 'n'
            );
#>>>

            if ($edit) {

                system "$lconfig{editor} \Q$abs_file\E";

                if ($?) {
                    note(":: $lconfig{editor} exited with code: " . exit_code());
                    next;
                }
            }
        }
    }

    say '' if !$lconfig{noedit};

    return 1;
}

sub run_makepkg_command ($pkg, @args) {

    system join(' ', $lconfig{makepkg_command}, @args);

    if ($?) {
        note(":: Unable to build <<$pkg>> - makepkg exited with code: " . exit_code());

        if (not $lconfig{ask_for_retry}) {
            main_quit(65) if $lconfig{asdeps};
            return;
        }

        if ($term->ask_yn(prompt => "$c{bold}=>> Try again?$c{reset}", default => 'n')) {
            local $lconfig{nopull} = 1;
            delete $seen_package{lc $pkg};
            install_package($pkg);
        }
        else {
            if ($term->ask_yn(prompt => "$c{bold}=>> Exit now?$c{reset}", default => 'y')) {
                main_quit(65);    # Package not installed
            }
            else {
                return;
            }
        }
    }

    return 1;
}

sub install_local_package ($name, $tarball, @pacman_args) {

    return 1 if $already_installed{lc $name};

    # Handle `--noinstall` option
    if ($lconfig{noinstall}) {
        if ($lconfig{movepkg}) {
            $already_installed{lc $name} = 1;
            move_built_package($tarball);
            return 1;
        }
        return 1;
    }

    # Install package with pacman
    if (execute_pacman_command(1, $tarball, @pacman_args)) {

        # Mark package as installed
        $already_installed{lc $name} = 1;

        # Move package (with `--movepkg`)
        if ($lconfig{movepkg}) {
            move_built_package($tarball);
        }

        return 1;
    }

    return;
}

sub install_local_packages ($packages, $pacman_args) {

    # Redirect to the main `install_local_package()` in `--noinstall` mode
    if ($lconfig{noinstall}) {
        foreach my $name (keys %$packages) {
            install_local_package($name, $packages->{$name}, @$pacman_args);
        }
        return 1;
    }

    my @packages_to_install = grep { not $already_installed{lc $_} } keys %$packages;

    # Install multiple packages with a single pacman command
    if (execute_pacman_command(1, @{$packages}{@packages_to_install}, @$pacman_args)) {

        # Mark packages as installed
        foreach my $name (@packages_to_install) {

            $already_installed{lc $name} = 1;

            # Move packages (with `--movepkg`)
            if ($lconfig{movepkg}) {
                move_built_package($packages->{$name});
            }
        }

        return 1;
    }

    return;
}

sub install_built_package ($pkg, @args) {

    if ($lconfig{install_built_with_noconfirm}) {
        push @args, '--noconfirm';
    }

    # Get the list of all built packages
    chomp(my @packagelist = map { decode_utf8($_) } `/usr/bin/makepkg --packagelist`);

    my %packages;
    foreach my $path (@packagelist) {
        if ($path =~ m{(?:.*/|^)(.*?)$pkg_suffix_re}oi) {
            my $pkgname = $1;
            $packages{lc $pkgname}      = $path;
            $already_built{lc $pkgname} = 1;
        }
    }

    if (not exists($packages{lc $pkg})) {
        note(":: Unable to find a built tarball for <<$pkg>>");
        $? = 2;
        return;
    }

    my $install_flag = $lconfig{install_built_with_Ud} ? '-Ud' : '-U';

    # When the list contains multiple packages, then ask the user which ones to install
    if (    scalar(grep { not $already_installed{lc $_} } keys(%packages)) > 1
        and $lconfig{split_packages}
        and !$lconfig{noinstall}) {

        say '';
        msg(":: This group contains <<" . scalar(keys %packages) . ">> packages:");
        say "\n" . join("\n", map { "   $_" } sort keys %packages), "\n";

        if (
            $term->ask_yn(prompt  => "$c{bold}=>> Do you want to install other packages from this group?$c{reset}",
                          default => 'n')
          ) {

            my @reply = $term->get_reply(
                                         print_me => "\n$c{bold}=>> Select which packages to install$c{reset}",
                                         prompt   => "$c{bold}Install$c{reset}",
                                         choices  => [sort keys %packages],
                                         default  => [sort keys %packages],
                                         multi    => 1,
                                        );

            # Install the selected packages
            return install_local_packages({map { lc($_) => $packages{lc($_)} } @reply}, [$install_flag, @args]);
        }
    }

    # Otherwise, install the main package only
    install_local_package($pkg, $packages{lc $pkg}, $install_flag, @args);
}

sub build_and_install_package ($pkg, @args) {

    if ($lconfig{nobuild}) {
        if ($lconfig{noinstall}) {
            $already_installed{lc $pkg} = 1;
            return 1;
        }
        $already_built{lc $pkg} = 1;
        return install_built_package($pkg, @args);
    }

    # Check to see if the current package was
    # already built, so we don't build it twice.
    if ($already_built{lc $pkg}) {
        return install_built_package($pkg, @args);
    }

    #
    ## Concept for installing packages directly with `makepkg`.
    ## However, this concept cannot install individual parts of a split package.
    ## See: https://github.com/trizen/trizen/issues/6
    #
#<<<
    #~ if (not $lconfig{noinstall}) {
        #~ push @args, '--install';
    #~ }

    #~ if ($lconfig{noconfirm}) {
        #~ push @args, '--noconfirm';
    #~ }

    #~ if ($lconfig{needed}) {
        #~ push @args, '--needed';
    #~ }

    #~ if (run_makepkg_command($pkg, @args)) {
        #~ $already_built{lc $pkg} = 1;
        #~ move_built_package($pkg) if $lconfig{movepkg};
        #~ return 1;
    #~ }
#>>>

    # Build the package and install/move it
    if (run_makepkg_command($pkg)) {
        $already_built{lc $pkg} = 1;    # mark the package as built
        return install_built_package($pkg, @args);
    }

    return;
}

sub is_lib32 ($pkg) {
    if ($arch ne 'x86_64') {
        return 1 if $pkg =~ /^lib32-\w/i;
    }
    return;
}

sub is_vcs_package ($pkg) {
    $pkg =~ /-(?:git|svn|bzr|cvs|hg|darcs|nightly(?:|[-_].+))\z/i;
}

sub install_packages_from_repo (@pkgs) {
    my @args = ('-S', get_pacman_arguments('long', qw(noconfirm force needed quiet)));

    if ($lconfig{asdeps}) {
        push @args, '--asdeps';
    }
    elsif ($lconfig{asexplicit}) {
        push @args, '--asexplicit';
    }

    return execute_pacman_command(1, @args, @pkgs);
}

sub install_package ($pkg) {

    # Avoid circular dependencies
    if ($seen_package{lc $pkg}) {
        return 1;
    }

    # Mark as seen
    local $seen_package{lc $pkg} = 1;

    msg(":: Current directory is: " . rel2abs(curdir())) if $lconfig{debug};

    my $info = download_package($pkg, $lconfig{clone_dir});
    if (ref($info) eq 'HASH') {
        msg(":: <<$pkg>> exists in AUR!") if $lconfig{debug};
    }
    elsif (not($lconfig{aur}) and is_available_in_pacman_repo($pkg)) {
        msg(":: <<$pkg>> is available in pacman's repository...") if $lconfig{debug};
        return install_packages_from_repo($pkg);
    }
    else {
        note(":: <<$pkg>> not found.");
        $? = 1;
        return;
    }

    if (my $version = package_is_installed($pkg)) {

        msg(":: <<$pkg>> is already installed!") if $lconfig{debug};

        if ($lconfig{needed}) {

            # Ignore the `--needed` flag when `--devel` is used and the package is a VCS package
            if ($lconfig{devel} and is_vcs_package($pkg)) {
                msg(":: <<$pkg>> is a VCS package -- installing");
            }

            # Compare the local version against the AUR version
            elsif (versioncmp($version, $info->{Version}) >= 0) {
                msg(":: <<$pkg>> is installed and up-to-date -- skipping");
                return 1;    # package is installed and up-to-date
            }

            # Package seems to be out-of-date
            else {
                note(":: <<$pkg>> seems to be {!out-of-date!} -- installing") if $lconfig{debug};
            }
        }
    }

    say '';
    msg(":: Package: $info->{Name}");
    msg(":: AUR URL: " . sprintf($aur_package_id_url, $info->{ID}));

    my @args = get_pacman_arguments('long', qw(noconfirm force needed));

    # Package will be installed as dependency
    if ($lconfig{asdeps}) {
        msg(":: <<$pkg>> will be installed as dependency...") if $lconfig{debug};
        push @args, '--asdeps';
    }

    # Package will be explicitly installed
    elsif ($lconfig{asexplicit}) {
        msg(":: <<$pkg>> will be explicitly installed...") if $lconfig{debug};
        push @args, '--asexplicit';
    }

    # When a package is already built, install it without building it again.
    if ($already_built{lc $pkg}) {
        msg(":: <<$pkg>> is already built -- installing") if $lconfig{debug};
        return install_built_package($pkg, @args);
    }

    if ($lconfig{show_comments}) {
        my @comments = get_comments($info->{ID});
        print "\n", join("\n", @comments) if @comments;
    }

    # Show and edit PKGBUILD and other -T files
    show_and_edit_text_files($info);

    # Recompute dependencies
    if ($lconfig{recompute_deps}) {
        recompute_dependencies($info);
    }

    # Display package info
    if (not $lconfig{noinfo}) {
        show_info($info);
    }

    foreach my $pkgname (
        grep { defined($_) && /^\w/ }

        # MakeDepends
        (exists($info->{MakeDepends}) ? @{$info->{MakeDepends}} : ()),

        # CheckDepends
        (exists($info->{CheckDepends}) ? @{$info->{CheckDepends}} : ()),

        # Depends
        (exists($info->{Depends}) ? @{$info->{Depends}} : ()),
      ) {

        my ($name, $cmp, $version) = parse_package_name_and_version($pkgname);

        if (is_lib32($name)) {
            msg(":: Ignoring lib32-* package arch '$arch': $name") if $lconfig{debug};
            next;
        }

        my $is_up_to_date = 0;
        my $local_version = package_is_installed($name);

        if (defined($local_version) and defined($version)) {
            if ($cmp eq '<') {
                if (versioncmp($local_version, $version) < 0) {
                    $is_up_to_date = 1;
                }
            }
            elsif ($cmp eq '<=') {
                if (versioncmp($local_version, $version) <= 0) {
                    $is_up_to_date = 1;
                }
            }
            elsif ($cmp eq '>') {
                if (versioncmp($local_version, $version) > 0) {
                    $is_up_to_date = 1;
                }
            }
            elsif ($cmp eq '>=') {
                if (versioncmp($local_version, $version) >= 0) {
                    $is_up_to_date = 1;
                }
            }
            elsif ($cmp eq '=' or $cmp eq '==') {
                if (versioncmp($local_version, $version) == 0) {
                    $is_up_to_date = 1;
                }
            }
        }

        if ($already_installed{lc $name} or (defined($local_version) ? defined($version) ? $is_up_to_date : 1 : 0)) {
            msg(":: Dependency <<$pkgname>> is already installed -- skipping") if $lconfig{debug};
        }
        elsif (is_available_in_pacman_repo($pkgname)) {
            msg(":: Dependency <<$pkgname>> is available in pacman's repository -- skipping") if $lconfig{debug};
        }
        else {

            # Check to see if the dependency is already provided by some other installed package.
            if (not defined($local_version)) {    # dependency is not installed

                if (package_is_provided($pkgname)) {
                    msg(":: Dependency <<$name>> is provided by other package -- skipping") if $lconfig{debug};
                    next;
                }
            }

            msg(":: Trying to install dependency: <<$name>>") if $lconfig{debug};

            # Activate `dependency` mode
            local $lconfig{asdeps} = 1;

            install_package($name) or do {
                note(":: Dependency not found: <<$name>>");
                next;
            };
        }
    }

    chdir($info->{_localpath});

    msg(":: Current directory is: " . rel2abs(curdir())) if $lconfig{debug};
    msg(":: Building and installing <<$pkg>>...") if $lconfig{debug};

    return build_and_install_package($pkg, @args);
}

sub clean_cache () {

    if (!$lconfig{aur}) {
        execute_pacman_command(1, get_pacman_arguments('all', qw(sync clean noconfirm force quiet)));
    }

    if ($lconfig{regular} or $lconfig{r}) {
        return 1;
    }

    opendir(my $dir_h, $lconfig{clone_dir})
      or error(":: Cannot access the AUR clone directory: {!$!!}");

    my $tmp_clonedir = ($lconfig{clone_dir} =~ m{^/tmp/});

    if (not $tmp_clonedir) {

#<<<
        $term->ask_yn(
            prompt  => "$c{bold}=>> Remove the content of $lconfig{clone_dir}?$c{reset}",
            default => 'n'
        ) || return;
#>>>

    }

    msg(":: Removing the content of <<$lconfig{clone_dir}>>...");

    foreach my $name (readdir($dir_h)) {

        next if substr($name, 0, 1) eq '.';    # skip dot files
        next if $name !~ /^[a-zA-Z0-9\-_+@.]+\z/;    # validate directory name

        my $dir = catdir($lconfig{clone_dir}, $name);

        if (-d $dir and -d catdir($dir, '.git')) {

            # When the directory does not contain a PKGBUILD file, ask the user about it
            if (not $tmp_clonedir and not -f catfile($dir, 'PKGBUILD')) {
                $term->ask_yn(prompt  => "$c{bold}=>> Remove the content of $dir?$c{reset}",
                              default => 'n')
                  || next;
            }

            msg(":: Removing <<$dir>>...") if $lconfig{debug};
            rmtree($dir) or note(":: Cannot remove directory <<$dir>>: {!$!!}");
        }
    }

    closedir($dir_h);
    msg(":: Done!");
}

sub select_packages_from_input ($input, $items, $default = [], $empty_for_all = 0) {

    my $number = qr/[0-9]{1,4}/;

    my @indices = (
        map {
                /^\^($number)(?:\.\.|-)($number)\z/ ? (map { -$_ } ($1 > $2 ? reverse($2 .. $1) : ($1 .. $2)))
              : /^(-?$number)(?:\.\.|-)(-?$number)\z/ ? ($1 > $2 ? reverse($2 .. $1) : ($1 .. $2))
              : $_
          }
          split(/[,\s]+/, $input)
    );

    my %unselected = (
                      map { ($items->{$_} => 1) }
                      grep { exists $items->{$_} }
                      map { substr($_, 1) }
                      grep { /^[-^]./ } @indices
                     );

    my @selected_packages = (
                             map  { $items->{$_} }
                             grep { exists $items->{$_} } @indices
                            );

    if (!@selected_packages and keys %unselected) {
        @selected_packages = @$default;
    }

    if (!@selected_packages and $empty_for_all and not any { /^[^^-]/ } @indices) {
        @selected_packages = @$default;
    }

    @selected_packages = grep { not exists $unselected{$_} } @selected_packages;

    return @selected_packages;
}

sub format_package_result ($package) {

    my $repo = $package->{Repository} // 'aur';

    my $is_installed_string = $lconfig{aur_results_show_installed}
      ? do {

        my $up_to_date     = 1;
        my $installed_text = $package->{Installed} // 'installed';

        my $installed_version = (
                                   $package->{Installed}
                                 ? $package->{Version}
                                 : package_is_installed($package->{Name}, 0)
                                );

        if ($package->{Installed} and $installed_text =~ /^(.+?): (.+)/) {
            $up_to_date        = 0;
            $installed_text    = $1;
            $installed_version = $2;
        }
        elsif (defined($installed_version) and $repo eq 'aur') {
            $up_to_date = versioncmp($installed_version, $package->{Version}) >= 0;
        }

        defined($installed_version)
          ? $up_to_date
              ? " [$c{bgreen}$installed_text$c{reset}]"
              : " [$c{bgreen}$installed_text$c{reset}: $c{bred}$installed_version$c{reset}]"
          : q{};
      }
      : q{};

    # Formatting string
    my $result_string =
      ($repo eq 'aur' ? $c{bpurple} : $c{bcyan}) . "$repo/$c{reset}$c{bold}%s$c{reset} $c{bblue}%s$c{reset}%s%s%s%s%s%s%s";

    # Normalize the popularity percentage
    $package->{Popularity} = sprintf("%.2f", $package->{Popularity} // 0);

#<<<
    return (
        sprintf($result_string,
          $package->{Name},
          $package->{Version},
          ($repo ne 'aur' && $package->{Group}                ? " [$c{bcyan}$package->{Group}$c{reset}]"      : q{}),
          $is_installed_string,
          ($repo eq 'aur' && $package->{OutOfDate}            ? " [$c{bred}out-of-date$c{reset}]"             : q{}),
          ($repo eq 'aur' && !$package->{Maintainer}          ? " [$c{bred}unmaintained$c{reset}]"            : q{}),
          ($repo eq 'aur' && $lconfig{aur_results_votes}      ? " [$c{bold}$package->{NumVotes}+$c{reset}]"   : q{}),
          ($repo eq 'aur' && $lconfig{aur_results_popularity} ? " [$c{bold}$package->{Popularity}%$c{reset}]" : q{}),
          (
            $repo eq 'aur' && $lconfig{aur_results_last_modified}
            ? localtime($package->{LastModified}) =~ /^\w+ (\w+)\s+(\d+)\s+.+? (\d+)$/ && " [$c{bold}$2 $1 $3$c{reset}]"
            : q{}
          )
        ),

        $package->{Description} // 'No description available...'
    );
#>>>
}

sub sort_aur_results (@results) {

    @results = sort { $a->{Name} cmp $b->{Name} } @results;

    if ($lconfig{aur_results_sort_by} =~ /popularity/i) {
        @results = sort { $a->{Popularity} <=> $b->{Popularity} } @results;
    }
    elsif ($lconfig{aur_results_sort_by} =~ /votes/i) {
        @results = sort { $a->{NumVotes} <=> $b->{NumVotes} } @results;
    }
    elsif ($lconfig{aur_results_sort_by} =~ /date/i) {
        @results = sort { $a->{LastModified} <=> $b->{LastModified} } @results;
    }

    if ($lconfig{aur_results_sort_order} =~ /ascending/i) {
        ## already in ascending order
    }
    else {
        @results = reverse(@results);
    }

    return @results;
}

sub print_package_results (@results) {

    foreach my $package (@results) {

        if ($lconfig{quiet}) {
            say $package->{Name};
            next;
        }

        printf("%s\n    %s\n", format_package_result($package));
    }

    return 1;
}

sub search_repo_packages (@keywords) {

    @keywords = map { quotemeta($_) } @keywords;

    my @pacman_args = get_pacman_arguments('long');
    my @lines = split(/\n/, decode_utf8(scalar `$lconfig{pacman_command} -Ss @pacman_args @keywords`));

    my @results;

    while (@lines) {
        my $pkgline = shift(@lines);

        my $description = shift(@lines);
        while (@lines and $lines[0] =~ /^\s/) {
            $description .= shift(@lines);
        }

        $description = join(' ', split(' ', $description));

        if ($pkgline =~ m{^(\S+)/(\S+) (\S+)(?: \((.*?)\))?(?: \[(.*?)\])?\s*\z}) {
            push @results,
              {
                Repository  => $1,
                Name        => $2,
                Version     => $3,
                Group       => $4,
                Installed   => $5,
                Description => $description,
              };
        }
        else {
            note(":: Unable to parse pacman result: $pkgline");
        }
    }

    return @results;
}

sub search_packages (@keywords) {

    my @repo_results;

    if (not $lconfig{aur}) {
        push @repo_results, search_repo_packages(@keywords);
    }

    if ($lconfig{regular} or $lconfig{r}) {
        return @repo_results;
    }

    my @aur_results;
    foreach my $key (map { uri_escape_utf8($_) } grep { length() > 1 } @keywords) {
        push @aur_results, json2perl(get("$aur_rpc_base_url?v=$AUR_V&type=search&arg=$key") // next);
    }

    my %seen;
    my @keys_re = map { qr/\Q$_\E/i } @keywords;

    my @matched_aur_results;
    foreach my $results (@aur_results) {

        ref($results->{results}) eq 'ARRAY' or next;

        foreach my $result (@{$results->{results}}) {
            next if $seen{$result->{Name}}++;

#<<<
            next if any {
                      not(($result->{Name}        // '') =~ $_)
                  and not(($result->{Description} // '') =~ $_)
            } @keys_re;
#>>>

            push @matched_aur_results, $result;
        }
    }

    $? = 0 if @matched_aur_results;

    my @results = (@repo_results, sort_aur_results(@matched_aur_results));

    if ($lconfig{flip_results}) {
        @results = reverse(@results);
    }

    return @results;
}

sub interactive_search_and_install (@keywords) {

    my @results = search_packages(@keywords);

    if (!@results) {
        note(":: No packages found...");
        return;
    }

    my $index_width = length(scalar @results);

    my @indices = (0 .. $#results);

    if ($lconfig{flip_indices}) {
        @results = reverse(@results);
        @indices = reverse(@indices);
    }

    my %items;

    foreach my $i (@indices) {
        my $package = $results[$i];

        $items{$i + 1} = $package;
        $package->{Repository} //= 'aur';

        if ($lconfig{quiet}) {
            printf("$c{bold}%*d$c{reset} %s\n", $index_width, $i + 1, $package->{Name});
            next;
        }

#<<<
        my ($name, $description) = format_package_result($package);
        printf("$c{bgreen}%*d$c{reset} %s\n%s%s\n", $index_width, $i + 1, $name, ' ' x ($index_width + 3), $description);
#>>>
    }

    @items{map { lc($_->{Name}) } @results} = @results;
    @items{map { lc($_->{Repository} . '/' . $_->{Name}) } @results} = @results;

    my $input = $term->readline("\n$c{bold}=>> Select packages to install$c{reset}\n> ") // return;
    my @selected_packages = select_packages_from_input(lc($input), \%items, \@results);

    my @aur_packages;
    my @repo_packages;

    foreach my $package (@selected_packages) {
        if ($package->{Repository} eq 'aur') {
            push @aur_packages, $package;
        }
        else {
            push @repo_packages, "$package->{Repository}/$package->{Name}";
        }
    }

    install_packages_from_repo(@repo_packages) if @repo_packages;

    foreach my $package (@aur_packages) {
        local $lconfig{aur} = 1;
        install_package($package->{Name});
    }

    return 1;
}

sub list_aur_maintainer_packages ($maintainer) {
    my $results = json2perl(get("$aur_rpc_base_url?v=$AUR_V&type=msearch&arg=$maintainer") // return);
    ref($results->{results}) eq 'ARRAY' or return;
    my @maintainers_packages = @{$results->{results}};
    @maintainers_packages || return;
    print_package_results(sort_aur_results(@maintainers_packages));
}

sub update_local_packages (@repo_packages) {

#<<<
    if (not $lconfig{aur}) {
        execute_pacman_command(1, @repo_packages,
            get_pacman_arguments('all', qw(sync needed asdeps asexplicit sysupgrade noconfirm force quiet refresh)),
            map { "--ignore=$_" } keys(%ignored_pkgs)
        );
    }
#>>>

    if ($lconfig{regular} or $lconfig{r}) {
        return 1;
    }

    my %packages = map { split(' ', $_) } `$lconfig{pacman_command} -Qm`;
    my @pkgnames = sort keys %packages;

    my %exists_in_aur;
    @exists_in_aur{@pkgnames} = ();

    my @aur_results;

    while (@pkgnames) {

        my $url = "$aur_rpc_base_url?v=$AUR_V&type=multiinfo";

        while (length($url) < 4000 and @pkgnames) {
            $url .= '&arg[]=' . uri_escape_utf8(shift(@pkgnames));
        }

        my $multiinfo = json2perl(
            get($url) // do {
                note(":: Unable to get info for AUR packages...");
                $? = 1;
                return;
              }
        );

        ref($multiinfo->{results}) eq 'ARRAY' or return;

        push @aur_results, @{$multiinfo->{results}};
    }

    my @packages_for_update;

    foreach my $result (@aur_results) {

        ref($result) eq 'HASH' or next;

        my $pkgname = $result->{Name};
        my $version = $result->{Version};

        $exists_in_aur{$pkgname} = 1;

        if ($lconfig{show_ood} and $result->{OutOfDate}) {
            note(":: <<$pkgname>> has been flagged {!out-of-date!}!");
        }

        if ($lconfig{show_unmaintained} and not $result->{Maintainer}) {
            note(":: <<$pkgname>> is {!unmaintained!}!");
        }

        my $is_vcs_package = is_vcs_package($pkgname);
        my $local_vcmp = versioncmp($packages{$pkgname}, $version);

        if ($local_vcmp < 0 or ($lconfig{devel} and $is_vcs_package)) {

            if (exists $ignored_pkgs{$pkgname}) {
                note(  ":: <<$pkgname>> -- ignoring upgrade ({!$packages{$pkgname}!} => <<"
                     . (($is_vcs_package && $local_vcmp >= 0) ? 'latest' : $version)
                     . ">>)");
                next;
            }

            push @packages_for_update,
              {
                name       => $pkgname,
                version    => $version,
                is_vcs     => $is_vcs_package,
                local_vcmp => $local_vcmp,
              };
        }
        elsif ($version ne $packages{$pkgname}) {
            note(":: <<$pkgname>> has a different version in AUR!" . " ({!$packages{$pkgname}!} != <<$version$c{reset}>>)")
              if $lconfig{debug};
        }
    }

    # Notify when a package is not found in AUR
    # https://github.com/trizen/trizen/issues/24
    if ($lconfig{show_inexistent}) {
        foreach my $pkgname (sort keys %exists_in_aur) {
            if (not $exists_in_aur{$pkgname}) {
                note(":: <<$pkgname>> was {!not found!} in AUR -- skipping");
            }
        }
    }

    my $max_pkgname_width = max(map { length($_->{name}) } @packages_for_update);
    my $max_version_width = max(map { length($packages{$_->{name}}) } @packages_for_update);

    my %for_update;

    foreach my $i (0 .. $#packages_for_update) {

        my $package = $packages_for_update[$i];

        my $pkgname = $package->{name};
        my $version = $package->{version};

        $for_update{$i + 1} = $pkgname;
        $for_update{lc($pkgname)} = $pkgname;

        if ($lconfig{show_upgrades_only}) {
            $lconfig{quiet}
              ? printf("%s\n", $pkgname)
              : printf("$c{bold}%s$c{reset} $c{bgreen}%s$c{reset} -> $c{bgreen}%s$c{reset}\n",
                       $pkgname, $packages{$pkgname}, $version);
        }
        elsif ($package->{is_vcs} and $package->{local_vcmp} >= 0) {

            my $aur_color   = $package->{local_vcmp} == 0 ? $c{bgreen} : $c{byellow};
            my $local_color = $package->{local_vcmp} == 0 ? $c{bgreen} : $c{bblue};

#<<<
            printf(
                "$c{bblue}%2s$c{reset}. $c{bold}%*s$c{reset}: $local_color%*s$c{reset} $c{bold}<=>$c{reset} $aur_color%s$c{reset}\n",
                $i + 1, $max_pkgname_width, $pkgname, $max_version_width, $packages{$pkgname}, $version
            );
#>>>
        }
        else {
#<<<
            printf(
                "$c{bblue}%2s$c{reset}. $c{bold}%*s$c{reset}: $c{bred}%*s$c{reset} $c{bold}==>$c{reset} $c{bgreen}%s$c{reset}\n",
                $i + 1, $max_pkgname_width, $pkgname, $max_version_width, $packages{$pkgname}, $version
            );
#>>>
        }
    }

    # Handle `-Qua` mode
    if ($lconfig{show_upgrades_only}) {
        $? = 1 if not @packages_for_update;
        return 1;
    }

    # When piped in interactive mode, just return.
    # e.g.: trizen -Su --aur | wc -l
    if (not $lconfig{noconfirm} and not $stdout_on_tty) {
        return 1;
    }

    if (not keys %for_update) {
        msg(":: No AUR updates found...");
        return 1;
    }

    my $input = (
                 $lconfig{noconfirm}
                 ? ''
                 : $term->readline("\n$c{bold}=>> Select packages for upgrade (<ENTER> for all)$c{reset}\n> ")
                ) // return;

    my @selected_packages = select_packages_from_input(lc($input), \%for_update, [map { $_->{name} } @packages_for_update], 1);

    foreach my $pkgname (@selected_packages) {
        if (install_package($pkgname)) {
            msg(":: <<$pkgname>> has been upgraded!") if $lconfig{debug};
        }
        else {
            note(":: <<$pkgname>> has {!NOT!} been upgraded!");
        }
    }

    return 1;
}

sub show_stats {
    opendir my $dir_h, $lconfig{pacman_local_dir}
      or error(":: Unable to open directory <<$lconfig{pacman_local_dir}>>: {!$!!}");

    my ($total_size, $num_of_pkgs, %dependencies, %reason_deps);

    my %packages;
    my $append_package = sub ($date, $pkg, $key) {

        $packages{$key}{new} //= [[q{}, 0]];
        $packages{$key}{old} //= [[q{}, 'inf']];

        if ($date < $packages{$key}{old}[-1][1]) {
            unshift @{$packages{$key}{old}}, [$pkg, $date];
            @{$packages{$key}{old}} = sort { $a->[1] <=> $b->[1] } @{$packages{$key}{old}};
            pop @{$packages{$key}{old}} if @{$packages{$key}{old}} > $lconfig{packages_in_stats};
        }
        if ($date > $packages{$key}{new}[-1][1]) {
            unshift @{$packages{$key}{new}}, [$pkg, $date];
            @{$packages{$key}{new}} = sort { $b->[1] <=> $a->[1] } @{$packages{$key}{new}};
            pop @{$packages{$key}{new}} if @{$packages{$key}{new}} > $lconfig{packages_in_stats};
        }

        return 1;
    };

    while (defined(my $subdir = readdir $dir_h)) {

        next if $subdir eq q{.} or $subdir eq q{..};
        -d "$lconfig{pacman_local_dir}/$subdir" or next;

        ++$num_of_pkgs;
        my ($current_pkg) = $subdir =~ /^(.+?)-[^-]+-[^-]+$/;

        open my $fh, '<:utf8', catfile($lconfig{pacman_local_dir}, $subdir, 'desc') or next;
        while (defined(my $line = <$fh>)) {

            chomp($line);

            if ($line eq "%REASON%") {
                $reason_deps{$current_pkg} = ();
            }
            elsif ($line eq "%DEPENDS%") {
                while (defined(my $dep = <$fh>)) {
                    chomp $dep;
                    last if $dep eq q{};
                    $dependencies{strip_version($dep)} = ();
                }
            }
            elsif ($line eq "%PROVIDES%" and exists $reason_deps{$current_pkg}) {
                while (defined(my $provided = <$fh>)) {
                    chomp $provided;
                    last if $provided eq q{};
                    push @{$reason_deps{$current_pkg}}, strip_version($provided);
                }
            }
            elsif ($line eq "%SIZE%") {
                $total_size += <$fh>;
            }
            elsif ($line eq "%BUILDDATE%") {
                chomp(my $date = <$fh>);
                $append_package->($date, $current_pkg, 'built');
            }
            elsif ($line eq "%INSTALLDATE%") {
                chomp(my $date = <$fh>);
                $append_package->($date, $current_pkg, 'installed');
            }
        }
    }
    closedir $dir_h;

    my $as_dep_packages = keys %reason_deps;

    print <<"STATS";
$c{bblue}::$c{reset} $c{bold}Total installed packages:$c{byellow} $num_of_pkgs$c{reset}
$c{bblue}::$c{reset} $c{bold}Explicitly installed packages:$c{byellow} ${\($num_of_pkgs - $as_dep_packages)}$c{reset}
$c{bblue}::$c{reset} $c{bold}Asdep installed packages:$c{byellow} $as_dep_packages$c{reset}
$c{bblue}::$c{reset} $c{bold}Theoretical space used by packages:$c{byellow} ${\int $total_size / 1024**2} MB$c{reset}\n
$c{bblue}::$c{reset} $c{bold}Oldest built packages:$c{byellow} @{[map { $_->[0] } @{$packages{built}{old}}]}$c{reset}
$c{bblue}::$c{reset} $c{bold}Newest built packages:$c{byellow} @{[map { $_->[0] } @{$packages{built}{new}}]}$c{reset}\n
$c{bblue}::$c{reset} $c{bold}Oldest installed packages:$c{byellow} @{[map { $_->[0] } @{$packages{installed}{old}}]}$c{reset}
$c{bblue}::$c{reset} $c{bold}Newest installed packages:$c{byellow} @{[map { $_->[0] } @{$packages{installed}{new}}]}$c{reset}\n
STATS

    print "$c{bblue}::$c{reset} $c{bold}Unneeded packages:$c{byellow}";
    while (my ($key, $value) = each %reason_deps) {
        next if exists $dependencies{$key};
        if (ref($value) eq 'ARRAY') {
            next if first { exists($dependencies{$_}) } @$value;
        }
        print qq{ $key};
    }
    say $c{reset};

    main_quit();
}

sub _parse_pkgname ($pkgref) {

    # Strip `aur/` from `aur/<pkgname>`
    if ($$pkgref =~ m{^aur/(.+)}) {
        $$pkgref = $1;
        return 1;
    }

    return;
}

#
## MAIN
#

if ($lconfig{S} or $lconfig{sync}) {    # -S

    if ($lconfig{l} or $lconfig{local}) {    # -Sl
        $lconfig{nopull}    = 1;
        $lconfig{clone_dir} = rel2abs(curdir());
    }

    if ($lconfig{list}) {
        execute_pacman_command(0, get_pacman_arguments('all', qw(list)), splice(@keyword_arguments));
    }

    if ($lconfig{h} or $lconfig{help}) {     # -Sh
        sync_help();
    }

    if ($lconfig{c} or $lconfig{clean}) {    # -Sc
        clean_cache();
    }

    if ($lconfig{s} or $lconfig{search}) {    # -Ss
        print_package_results(search_packages(splice @keyword_arguments));
    }

    if ($lconfig{i} or $lconfig{info}) {      # -Si
        foreach my $pkg (splice @keyword_arguments) {

            local $lconfig{aur} = 1 if _parse_pkgname(\$pkg);

            if (!$lconfig{aur} && is_available_in_pacman_repo($pkg)) {
                execute_pacman_command(0, get_pacman_arguments('all', qw(info sync)), $pkg);
            }
            else {
                my $info = info_for_package($pkg) // do {
                    note(":: <<$pkg>> not found.");
                    $? = 1;
                    next;
                };
                show_info($info);
            }
        }
    }

    if ($lconfig{m} or $lconfig{maintainer}) {    # -Sm
        foreach my $maintainer (splice @keyword_arguments) {
            msg(":: AUR packages maintained by <<$maintainer>>") if $lconfig{debug};
            list_aur_maintainer_packages($maintainer)
              or do {
                note(":: Maintainer not found: $maintainer");
                $? = 1;
                next;
              };
        }
    }

    if ($lconfig{p} or $lconfig{pkgbuild} or $lconfig{print}) {    # -Sp
        foreach my $pkg (splice @keyword_arguments) {

            # For repo packages, display the download URL only
            if (is_available_in_pacman_repo($pkg)) {
                execute_pacman_command(0, get_pacman_arguments('all', qw(sync print)), $pkg);
                next;
            }

            local $lconfig{aur} = 1 if _parse_pkgname(\$pkg);

            download_package($pkg, $lconfig{clone_dir}) or do {
                note(":: <<$pkg>> not found.");
                $? = 1;
                next;
            };

            open my $fh, '<:utf8', 'PKGBUILD'
              or do {
                note(":: Cannot open PKGBUILD of package <<$pkg>>: {!$!!}");
                $? = 2;
                next;
              };

            say '';
            msg(":: PKGBUILD of <<$pkg>>");
            say '';
            say <$fh>;

            close $fh;
        }
    }

    my @aur_packages;
    my @repo_packages;

    foreach my $pkg (splice @keyword_arguments) {
        local $lconfig{aur} = 1 if _parse_pkgname(\$pkg);

        if (!$lconfig{aur} and is_available_in_pacman_repo($pkg)) {
            push @repo_packages, $pkg;
        }
        else {
            push @aur_packages, $pkg;
        }
    }

    if ($lconfig{u} or $lconfig{sysupgrade}) {    # -Su
        update_local_packages(splice(@repo_packages));
    }

    elsif ($lconfig{y} or $lconfig{refresh}) {    # -Sy
        execute_pacman_command(1, splice(@repo_packages),
                               get_pacman_arguments('all', qw(sync needed asdeps asexplicit refresh force quiet noconfirm)));
    }

    install_packages_from_repo(@repo_packages) if @repo_packages;

    foreach my $pkg (@aur_packages) {             # -S only
        if (install_package($pkg)) {
            msg(":: <<$pkg>> has been successfully installed!") if $lconfig{debug};
        }
        else {
            note(":: Cannot install package: $pkg") if $lconfig{debug};
        }
    }
}
elsif ($lconfig{C} or $lconfig{comments}) {       # -C

    foreach my $pkg (@keyword_arguments) {
        local $lconfig{aur} = 1 if _parse_pkgname(\$pkg);
        my $info = info_for_package($pkg) // do {
            note(":: <<$pkg>> not found.");
            $? = 1;
            next;
        };

        say '';
        msg(":: Package: $pkg");
        msg(":: AUR URL: " . sprintf($aur_package_id_url, $info->{ID}));
        say '';

        foreach my $comment (get_comments($info->{ID})) {
            say $comment;
        }
    }
}
elsif ($lconfig{G} or $lconfig{get}) {    # -G

    if ($lconfig{h} or $lconfig{help}) {    # -Gh
        get_help();
    }

    my $curdir = rel2abs(curdir());

    if ($lconfig{d} or $lconfig{with_deps}) {    # -Gd

        foreach my $pkg (@keyword_arguments) {

            local $lconfig{aur}                      = 1 if _parse_pkgname(\$pkg);
            local $lconfig{clone_dir}                = $curdir;
            local $lconfig{nobuild}                  = 1;
            local $lconfig{noinstall}                = 1;
            local $lconfig{noconfirm}                = 1;
            local $lconfig{noedit}                   = 1;
            local $lconfig{noinfo}                   = 1;
            local $lconfig{recompute_deps}           = 0;
            local $lconfig{show_build_files_content} = 0;

            if (!install_package($pkg)) {
                note(":: <<$pkg>> not found.");
                $? = 1;
                next;
            }
        }
    }
    else {    # -G only
        foreach my $pkg (@keyword_arguments) {
            local $lconfig{aur} = 1 if _parse_pkgname(\$pkg);
            msg(":: Cloning package: $pkg") if $lconfig{debug};
            if (!download_package($pkg, $curdir)) {
                note(":: <<$pkg>> not found.");
                $? = 1;
                next;
            }
        }
    }
}
elsif ($lconfig{U} or $lconfig{upgrade}) {    # -U

    if ($lconfig{h} or $lconfig{help}) {
        execute_pacman_command(0, '-Uh');
        main_quit();
    }

    my @packages;
    my @args = get_pacman_arguments('all', qw(asdeps asexplicit noconfirm force needed));

    foreach my $pkg (@keyword_arguments) {
        if ($pkg =~ /$pkg_suffix_re/io and -f $pkg) {    # install from current dir
            push @packages, $pkg;
        }
        else {                                           # install from cache dir
            foreach my $dir (grep { -d } glob("$lconfig{clone_dir}/*")) {
                my $tarball = find_local_package($pkg, $dir) // next;
                msg(":: Found tarball: $tarball");
                push @packages, $tarball;
            }
        }
    }

    execute_pacman_command(1, qw(-U), @args, @packages) if @packages;
}
elsif ($lconfig{R} or $lconfig{remove}) {                # -R

    if ($lconfig{h} or $lconfig{help}) {
        execute_pacman_command(0, '-Rh');
        main_quit();
    }

    execute_pacman_command(1, get_pacman_arguments('all', qw(remove noconfirm)), @keyword_arguments);
}
elsif ($lconfig{D} or $lconfig{database}) {              # -D

    if ($lconfig{h} or $lconfig{help}) {
        execute_pacman_command(0, '-Dh');
        main_quit();
    }

    my @args = (get_pacman_arguments('all', qw(database asdeps asexplicit check noconfirm)), @keyword_arguments);

    if ($lconfig{k} or $lconfig{check}) {
        execute_pacman_command(0, @args);
    }
    else {
        execute_pacman_command(1, @args);
    }
}
elsif ($lconfig{F} or $lconfig{files}) {    # -F

    if ($lconfig{h} or $lconfig{help}) {
        execute_pacman_command(0, '-Fh');
        main_quit();
    }

    my @args = (get_pacman_arguments('all', qw(files quiet search refresh noconfirm regex)), @keyword_arguments);

    if ($lconfig{y} or $lconfig{refresh}) {
        execute_pacman_command(1, @args);
    }
    else {
        execute_pacman_command(0, @args);
    }
}
elsif ($lconfig{Q} or $lconfig{query}) {    # -Q

    if ($lconfig{h} or $lconfig{help}) {
        execute_pacman_command(0, '-Qh');
        main_quit();
    }

    if ($lconfig{aur} and ($lconfig{u} or $lconfig{upgrades})) {
        local $lconfig{aur}                = 1;
        local $lconfig{show_upgrades_only} = 1;
        update_local_packages();
    }
    else {
#<<<
        execute_pacman_command(0, get_pacman_arguments('all', qw(query check search info quiet noconfirm upgrades)), @keyword_arguments);
#>>>
    }
}
elsif ($lconfig{T} or $lconfig{deptest}) {    # -T

    if ($lconfig{h} or $lconfig{help}) {
        execute_pacman_command(0, '-Th');
        main_quit();
    }

    execute_pacman_command(0, get_pacman_arguments('all', qw(deptest noconfirm)), @keyword_arguments);
}
elsif ($lconfig{h} or $lconfig{help}) {
    help();
}
elsif (@keyword_arguments) {
    interactive_search_and_install(@keyword_arguments);
}

main_quit();
