#!/usr/bin/perl
#
# OPI - openSUSE Build Service Package Installer
#
# Author:     Guo Yunhe
# Website:    https://github.com/openSUSE-zh/opi
# License:    GPL-3.0

use strict;
use warnings;
use LWP::UserAgent;
use XML::LibXML;
use URI::Escape;
use Config;
use Config::Tiny;
use Term::ANSIColor;
use File::Temp;

#
# Check command line arguments
#
unless (scalar @ARGV) {
    print "Please specify query keywords. For example:\n";
    print "\topi pcsx2\n";
    exit 1;
}

#
# Check for packages not from OBS
#

# 'vs code' --> 'vscode'
my $serialized_query = lc(join('', @ARGV));

# Check Packman Codecs
if ( $serialized_query =~ m/(packman|codec)/ ) {
    install_packman_codecs();
    exit;
}

# Check VS Code
if ( $serialized_query =~ m/(visualstudiocode|vscode|vsc)/ ) {
    install_vs_code();
    exit;
}

# Check Skype
if ($serialized_query =~ m/skype/ ) {
    install_skype();
    exit;
}


#
# API configuration
#
my $obs_apiroot = 'https://api.opensuse.org';
my $pmbs_apiroot = 'https://pmbs.links2linux.de';

#
# Search packages
#
print "Searching...\n";

my @obs = search_published_binary('openSUSE', $obs_apiroot, @ARGV);
my @pmbs = search_published_binary('Packman', $pmbs_apiroot, @ARGV);

my @bins = sort_binaries(@obs, @pmbs);
my @binary_names = get_binary_names(@bins);

if (!scalar(@bins)) {
    print "No package found.\n";
    exit;
}

#
# Print package name options
#
print_package_names(@binary_names);


#
# Select a package name option
#
my $selected_name_number = type_a_number(1, scalar(@binary_names));
my $selected_name = $binary_names[$selected_name_number - 1];

print "You have selected package name: $selected_name\n";

my @binary_options = get_binary_by_name($selected_name, @bins);


#
# Print binary package options
#
print_binary_options(@binary_options);


#
# Select a binary package option
#
my $selected_binary_number = type_a_number(1, scalar(@binary_options));

my $selected_binary = $binary_options[$selected_binary_number - 1];

print "You have selected binary package: ";
print_binary_option($selected_binary);
print "\n";


#
# Install selected package
#
install_binary($selected_binary);


=begin functions

Get system information

=cut

sub trim {
    my $s = shift;
    $s =~ s/^\s+|\s+$//g;
    return $s
}

sub get_distribution {
    my $prefix = shift;
    my $config = Config::Tiny->read('/etc/os-release');
    my $name = $config->{_}->{NAME};
    my $version = $config->{_}->{VERSION};
    $name = trim(substr($name, 1, -1)); # Remove quotes and trailing spaces
    if ($version) {
        $version = trim(substr($version, 1, -1)); # Remove quotes
    }
    if ($name eq 'openSUSE Tumbleweed') {
        $name = 'openSUSE:Factory';
    } elsif ($name eq 'openSUSE Leap') {
        $name = 'openSUSE:Leap:' . $version;
    } elsif (substr($name, 0, 3) eq 'SLE') {
        $name = 'SLE' . $version;
    } else {
        print "Your distribution $name $version is not supported.\n";
        exit 1;
    }
    if ($prefix) {
        $name = 'openSUSE.org:' . $name;
    }
    return $name;
}

sub get_architecture {
    my $perl_arch = $Config{'archname'};

    if (substr($perl_arch, 0, 4) eq 'i386') {
        return 'i586';
    }

    $perl_arch =~ m/^([a-z0-9_]+)/;

    return $&;
}


=begin functions

Receive user inputs

=cut

sub type_a_number {
    my $min = shift;
    my $max = shift;
    my $message = shift;
    print $message || "Choose a number(0 to quit): ";
    my $typed_number = <STDIN>;
    chomp $typed_number;
    if ($typed_number =~ /^(\d+)$/) {
        $typed_number += 0; # Convert string to number
        if ($typed_number >= $min && $typed_number <= $max) {
            return $typed_number;
        }
    }
    if ($typed_number eq 0) {
            exit;
    }
    return type_a_number($min, $max, "Number must be between $min and $max. Please try again: ");
}

sub ask_yes_or_no {
    print $_[0];

    if (lc($_[1]) eq 'y') {
        print '(Y/n) ';
    } else {
        print '(y/N) ';
    }

    my $yes_no = <STDIN>;
    chomp $yes_no;
    $yes_no = lc(substr($yes_no, 0, 1));

    if (lc($_[1]) eq 'y') {
        return $yes_no ne 'n';
    } else {
        return $yes_no eq 'y';
    }
}

sub ask_keep_repo {
    my $repo = $_[0];
    unless ( ask_yes_or_no("Do you want to keep these repositories? ", 'y') ) {
        system "sudo zypper removerepo $repo";
    }
}

=begin functions

Print package lists

=cut

sub print_package_names {
    my $i = 1;
    foreach my $n (@_) {
        printf("%2d. %s\n", $i, $n);
        $i++;
    }
}

sub print_binary_options {
    my $i = 1;
    foreach my $b (@_) {
        print_binary_option($b, $i);
        $i++;
    }
}

sub print_binary_option {
    my $binary = shift;
    my $number = shift;
    my $color;
    my $symbol;
    if (is_official_project($binary->{project})) {
        $color = 'green';
        $symbol = '+';
    } elsif (is_personal_project($binary->{project})) {
        $color = 'red';
        $symbol = '!';
    } else {
        $color = 'cyan';
        $symbol = '?';
    }

    my $project = $binary->{project};
    my $obs_instance = $binary->{obs_instance};
    if ($obs_instance ne 'openSUSE') {
        $project = "$obs_instance $project";
    }

    my $colored_name = colored(substr($project, 0, 39) . ' ' . $symbol, $color);

    if ($number) {
        printf("%2d. %-50s | %-25s | %s\n", $number, $colored_name, substr($binary->{version}, 0, 25), $binary->{arch});
    } else {
        print $colored_name, " | ", $binary->{version}, " | ", $binary->{arch};
    }
}

=begin functions

Search OBS API

=cut

sub prepare_query_string {
    my $query_string = join "', '", @_;
    $query_string = "'" . $query_string . "'";
    $query_string =~ s/-/', '/ig;
    return $query_string;
}

sub search_published_binary {
    my $obs_instance = shift;
    my $obs_apiroot = shift;

    my $distribution = get_distribution($obs_instance ne 'openSUSE');

    my $proxy_root = 'https://guoyunhe.me/opi/proxy/index.php';

    my $endpoint = '/search/published/binary/id';

    my $query_string = prepare_query_string(@_);
    my $xpath = "contains-ic(\@name, $query_string) and path/project='$distribution'";

    # NOTE limit=0 fix 413 errors when searching php, test, etc.
    my $url = $obs_apiroot . $endpoint . '?match=' . uri_escape($xpath) . '&limit=0';

    my $proxy_url = $proxy_root . '?obs_api_link=' . uri_escape($url) . '&obs_instance=' . $obs_instance;

    my $req = HTTP::Request->new(GET => $proxy_url);
    my $ua = LWP::UserAgent->new;
    my $resp = $ua->request($req);
    if ($resp->is_success) {
        my $message = $resp->decoded_content;
        my @collection = ();

        if ($message =~ /^\s*$/) {
            return @collection;
        }

        my $dom = XML::LibXML->load_xml(string => $message);

        my $arch = get_architecture();

        foreach my $binary ($dom->findnodes('/collection/binary')) {
            my %binary_data;
            $binary_data{'obs_instance'} = $obs_instance;
            $binary_data{'name'} = $binary->getAttribute('name');
            $binary_data{'project'} = $binary->getAttribute('project');
            $binary_data{'package'} = $binary->getAttribute('package');
            $binary_data{'repository'} = $binary->getAttribute('repository');
            $binary_data{'version'} = $binary->getAttribute('version');
            $binary_data{'release'} = $binary->getAttribute('release');
            $binary_data{'arch'} = $binary->getAttribute('arch');
            $binary_data{'filename'} = $binary->getAttribute('filename');
            $binary_data{'filepath'} = $binary->getAttribute('filepath');
            $binary_data{'baseproject'} = $binary->getAttribute('baseproject');
            $binary_data{'type'} = $binary->getAttribute('type');

            # Filter out ghost binary
            # (package has been deleted, but binary still exists)
            if ( ! $binary_data{'package'} ) {
                next;
            }

            # Filter out branch projects
            if ( $binary_data{'project'} =~ /:branches:/m ) {
                next;
            }

            # Filter out Packman personal projects
            if (
                $binary_data{'obs_instance'} ne 'openSUSE'
                && is_personal_project($binary_data{'project'})
            ) {
                next;
            }

            # Filter out debuginfo, debugsource, buildsymbols packages
            if ( substr($binary_data{'name'}, -10) eq '-debuginfo' ) {
                next;
            } elsif ( substr($binary_data{'name'}, -12) eq '-debugsource' ) {
                next;
            } elsif ( substr($binary_data{'name'}, -13) eq '-buildsymbols' ) {
                next;
            }

            # Filter out source packages
            if ( $binary_data{'arch'} eq 'src' ) {
                next;
            }

            # Filter architecture
            unless ( $binary_data{'arch'} eq $arch || $binary_data{'arch'} eq 'noarch') {
                unless ( $binary_data{'arch'} eq 'i586' && $arch eq 'x86_64' ) {
                    next;
                }
            }

            push @collection, \%binary_data;
        }

        return @collection;
    }
    else {
        if ($resp->code == 413) {
            print "Please use different search keywords. Some short keywords cause OBS timeout.\n";
        } else {
            print "Network error. Please try later. (Error message: ", $resp->message, ")\n";
        }
        exit 1;
    }
}

=begin functions

Handle binary data

=cut

sub get_binary_names {
    my @names = ();
    foreach my $bin (@_) {
        my $name = $bin->{'name'};
        if (! grep /^$name$/, @names) {
            push @names, $name;
        }
    }
    return @names;
}

sub sort_binaries {
    return sort { -get_binary_weight($a) <=> -get_binary_weight($b) } @_;
}

sub get_binary_weight {
    my $binary = shift;
    my $weight = 0;

    if ( is_official_project($binary->{'project'}) ) {
        $weight += 20000;
    } elsif ( is_personal_project($binary->{'project'}) ) {
        $weight += 0;
    } else {
        $weight += 10000;
    }

    if ( $binary->{'name'} eq $binary->{'package'} ) {
        $weight += 1000;
    }

    my $dash_count = () = $binary->{'name'} =~ /-/g;
    $weight += 100 * (0.5**$dash_count);

    unless (get_architecture() eq 'x86_64' && $binary->{'arch'} eq 'i586') {
        $weight += 10;
    }

    $weight += - length $binary->{name};

    return $weight;
}

sub is_official_project {
    return substr($_[0], 0, 9) eq 'openSUSE:';
}

sub is_experimental_project {
    return !is_official_project(@_) && !is_personal_project(@_);
}

sub is_personal_project {
    return substr($_[0], 0, 5) eq 'home:' || substr($_[0], 0, 4) eq 'isv:';
}

sub get_binary_by_name {
    my $name = $_[0];
    my @binary_list = splice @_, 1;
    my @filtered_binary_list = ();

    foreach my $bin (@binary_list) {
        if ($name eq $bin->{'name'}) {
            push @filtered_binary_list, $bin;
        }
    }

    return @filtered_binary_list;
}

=begin functions

Add repository and install packages

=cut

sub install_binary {
    my $binary = shift;
    my $name = $binary->{name};
    my $obs_instance = $binary->{obs_instance};
    my $arch = $binary->{arch};
    my $project = $binary->{project};
    my $repository = $binary->{repository};

    # Install Packman packages
    if ($obs_instance eq 'Packman') {
        add_packman_repo();

        install_packman_packages("$name.$arch");
    }
    # Install official packages. Don't add repositories
    elsif (is_official_project($project)) {
        system "sudo zypper install $name.$arch";
    }
    # Install experimental and personal packages
    else {
        my $repo_url = "https://download.opensuse.org/repositories/$project/$repository/";
        my $repo_file = "$repo_url/$project.repo";
        system "sudo zypper addrepo --refresh $repo_file";
        # Change http:// to https://
        system "sudo sed -i 's~http://download.opensuse.org~https://download.opensuse.org~g' /etc/zypp/repos.d/*";
        system "sudo zypper refresh";
        my $install_options = "--allow-vendor-change --allow-arch-change --allow-downgrade --allow-name-change";
        $project =~ s/:/_/ig;
        system "sudo zypper install $install_options --from $project $name.$arch";

        ask_keep_repo( $project );
    }
}

sub install_packman_codecs {
    unless ( ask_yes_or_no("Do you want to install codecs from Packman repository? ", 'y') ) {
        return;
    }

    add_packman_repo(1);

    install_packman_packages(
        'ffmpeg',
        'gstreamer-plugins-bad',
        'gstreamer-plugins-libav',
        'gstreamer-plugins-ugly',
        'libavcodec58',
        'libavdevice58',
        'libavfilter7',
        'libavformat58',
        'libavresample4',
        'libavutil56',
        'vlc-codecs',
    );

    exit;
}

sub add_packman_repo {
    my $dup = shift;

    my $prefix = get_distribution();
    $prefix =~ s/:/_/ig;

    if ($prefix eq 'openSUSE_Factory') {
        $prefix = 'openSUSE_Tumbleweed';
    }

    system "sudo zypper addrepo --refresh --priority 90 --name Packman https://ftp.gwdg.de/pub/linux/misc/packman/suse/$prefix/ packman";
    system "sudo zypper refresh";

    if ($dup) {
        system "sudo zypper dist-upgrade --from packman --allow-downgrade --allow-vendor-change";
    }
}

sub install_packman_packages {
    my $packages = join ' ', @_;
    system "sudo zypper install --from packman $packages";
}

sub install_vs_code {
    unless ( ask_yes_or_no("Do you want to install VS Code from Microsoft repository? ", 'y') ) {
        return;
    }

    my $tmp_fh = new File::Temp( UNLINK => 1 );
    print $tmp_fh "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com/yumrepos/vscode\nenabled=1\ntype=rpm-md\ngpgcheck=1\ngpgkey=https://packages.microsoft.com/keys/microsoft.asc\n";
    system "sudo rpm --import https://packages.microsoft.com/keys/microsoft.asc";
    system "sudo mv $tmp_fh /etc/zypp/repos.d/vscode.repo";
    system "sudo zypper refresh";
    system "sudo zypper install code";

    ask_keep_repo( 'code' );

    exit;
}

sub install_skype {
    unless ( ask_yes_or_no("Do you want to install Skype from Microsoft repository? ", 'y') ) {
        return;
    }

    system "sudo zypper addrepo --refresh https://repo.skype.com/rpm/stable/skype-stable.repo";
    system "sudo zypper refresh";
    system "sudo zypper install skypeforlinux";

    ask_keep_repo( 'skype-stable' );

    exit;
}
