paru 0.99.2

Aur helper and pacman wrapper
use crate::config::Config;
use crate::fmt::{color_repo, print_indent};
use crate::install::install;
use crate::util::{input, NumberMenu};
use crate::{sprint, sprintln};

use ansi_term::Style;
use anyhow::{Context, Result};
use indicatif::HumanBytes;
use raur::{Raur, SearchBy};

pub fn search(config: &Config) -> Result<i32> {
    let quiet = config.args.has_arg("q", "quiet");
    let repo_pkgs = search_repos(config, &config.targets)?;
    for pkg in &repo_pkgs {
        print_alpm_pkg(config, pkg, quiet);
    }

    let targets = config
        .targets
        .iter()
        .map(|t| t.to_lowercase())
        .collect::<Vec<_>>();

    let pkgs = search_aur(config, &targets).context("aur search failed")?;
    for pkg in &pkgs {
        print_pkg(config, pkg, quiet)
    }
    Ok((repo_pkgs.is_empty() && pkgs.is_empty()) as i32)
}

fn search_repos<'a>(config: &'a Config, targets: &[String]) -> Result<Vec<alpm::Package<'a>>> {
    if targets.is_empty() || config.mode == "aur" {
        return Ok(Vec::new());
    }

    let mut ret = Vec::new();

    for db in config.alpm.syncdbs() {
        let pkgs = db.search(targets)?;
        ret.extend(pkgs);
    }

    Ok(ret)
}

fn search_aur(config: &Config, targets: &[String]) -> Result<Vec<raur::Package>> {
    if targets.is_empty() || config.mode == "repo" {
        return Ok(Vec::new());
    }

    let mut matches = Vec::new();

    let by = match config.search_by.as_str() {
        "name" => SearchBy::Name,
        "maintainer" => SearchBy::Maintainer,
        "depends" => SearchBy::Depends,
        "makedepends" => SearchBy::MakeDepends,
        "checkdepends" => SearchBy::CheckDepends,
        "optdepends" => SearchBy::OptDepends,
        _ => SearchBy::NameDesc,
    };

    if by == SearchBy::NameDesc {
        let target = targets.iter().max_by_key(|t| t.len()).unwrap();
        let pkgs = config.raur.search_by(target, by)?;
        matches.extend(pkgs);
        matches.retain(|p| {
            targets.iter().all(|t| {
                p.name.contains(t) | p.description.as_ref().unwrap_or(&String::new()).contains(t)
            })
        });
    } else if by == SearchBy::Name {
        let target = targets.iter().max_by_key(|t| t.len()).unwrap();
        let pkgs = config.raur.search_by(target, by)?;
        matches.extend(pkgs);
        matches.retain(|p| targets.iter().all(|t| p.name.contains(t)));
    } else {
        for target in targets {
            let pkgs = config.raur.search_by(target, by)?;
            matches.extend(pkgs);
        }
    }

    match config.sort_by.as_str() {
        "votes" => matches.sort_by(|a, b| b.num_votes.cmp(&a.num_votes)),
        "popularity" => matches.sort_by(|a, b| b.popularity.partial_cmp(&a.popularity).unwrap()),
        "id" => matches.sort_by_key(|p| p.id),
        "name" => matches.sort_by(|a, b| a.name.cmp(&b.name)),
        "base" => matches.sort_by(|a, b| a.package_base.cmp(&b.package_base)),
        "submitted" => matches.sort_by_key(|p| p.first_submitted),
        "modified" => matches.sort_by_key(|p| p.last_modified),
        _ => (),
    }

    Ok(matches)
}

fn print_pkg(config: &Config, pkg: &raur::Package, quiet: bool) {
    if quiet {
        sprintln!("{}", pkg.name);
        return;
    }

    let c = config.color;
    let stats = format!("+{} ~{:.2}", pkg.num_votes, pkg.popularity);
    sprint!(
        "{}/{} {} [{}]",
        color_repo("aur"),
        c.ss_name.paint(&pkg.name),
        c.ss_ver.paint(&pkg.version),
        c.ss_stats.paint(stats),
    );

    if let Ok(repo_pkg) = config.alpm.localdb().pkg(&pkg.name) {
        let installed = if repo_pkg.version().as_ref() != pkg.version {
            format!("[Installed: {}]", repo_pkg.version())
        } else {
            "[Installed]".to_string()
        };

        sprint!(" {}", c.ss_installed.paint(installed));
    }

    if pkg.maintainer.is_none() {
        sprint!(" {}", c.ss_orphaned.paint("[Orphaned]"));
    }

    sprint!("\n    ");
    let desc = pkg
        .description
        .as_deref()
        .unwrap_or_default()
        .split_whitespace();
    print_indent(Style::new(), 4, 4, config.cols, " ", desc);
}

fn print_alpm_pkg(config: &Config, pkg: &alpm::Package, quiet: bool) {
    if quiet {
        sprintln!("{}", pkg.name());
        return;
    }

    let c = config.color;
    let stats = format!(
        "{} {}",
        HumanBytes(pkg.download_size() as u64),
        HumanBytes(pkg.isize() as u64)
    );
    let ver: &str = pkg.version().as_ref();
    sprint!(
        "{}/{} {} [{}]",
        color_repo(pkg.db().unwrap().name()),
        c.ss_name.paint(pkg.name()),
        c.ss_ver.paint(ver),
        c.ss_stats.paint(stats),
    );

    if let Ok(repo_pkg) = config.alpm.localdb().pkg(pkg.name()) {
        let installed = if repo_pkg.version() != pkg.version() {
            format!("[Installed: {}]", repo_pkg.version())
        } else {
            "[Installed]".to_string()
        };

        sprint!(" {}", c.ss_installed.paint(installed));
    }

    if !pkg.groups().is_empty() {
        for group in pkg.groups() {
            sprint!(" {}", c.ss_orphaned.paint(" ("));
            sprint!(" {}", c.ss_orphaned.paint(group));
            sprint!(" {}", c.ss_orphaned.paint(")"));
        }
    }

    sprint!("\n    ");
    let desc = pkg.desc();
    let desc = desc.as_deref().unwrap_or_default().split_whitespace();
    print_indent(Style::new(), 4, 4, config.cols, " ", desc);
}

pub fn search_install(config: &mut Config) -> Result<i32> {
    let repo_pkgs = search_repos(config, &config.targets)?;
    let aur_pkgs = search_aur(config, &config.targets)?;
    let len = repo_pkgs.len() + aur_pkgs.len();
    let pad = len.to_string().len();
    let c = config.color;

    if len == 0 {
        sprintln!("no packages match search");
        return Ok(1);
    }

    if config.sort_mode == "topdown" {
        for (n, pkg) in repo_pkgs.iter().enumerate() {
            let n = format!("{:>pad$}", n + 1, pad = pad);
            sprint!("{} ", c.number_menu.paint(n));
            print_alpm_pkg(config, pkg, false)
        }

        for (n, pkg) in aur_pkgs.iter().enumerate() {
            let n = format!("{:>pad$}", n + repo_pkgs.len() + 1, pad = pad);
            sprint!("{}{} ", "", c.number_menu.paint(n));
            print_pkg(config, pkg, false)
        }
    } else {
        for (n, pkg) in aur_pkgs.iter().rev().enumerate() {
            let n = format!("{:>pad$}", len - n, pad = pad);
            sprint!("{} ", c.number_menu.paint(n));
            print_pkg(config, pkg, false)
        }

        for (n, pkg) in repo_pkgs.iter().rev().enumerate() {
            let n = format!("{:>pad$}", len - (n + aur_pkgs.len()), pad = pad);
            sprint!("{}{} ", "", c.number_menu.paint(n));
            print_alpm_pkg(config, pkg, false)
        }
    }

    let input = input(config, "Packages to install (eg: 1 2 3, 1-3 or ^4): ");
    let menu = NumberMenu::new(&input);
    let mut pkgs = Vec::new();

    if config.sort_mode == "topdown" {
        for (n, pkg) in repo_pkgs.iter().enumerate() {
            if menu.contains(n + 1, "") {
                pkgs.push(pkg.name().to_string())
            }
        }
        for (n, pkg) in aur_pkgs.iter().enumerate() {
            if menu.contains(n + repo_pkgs.len() + 1, "") {
                pkgs.push(pkg.name.clone())
            }
        }
    } else {
        for (n, pkg) in aur_pkgs.iter().rev().enumerate() {
            if menu.contains(len - n, "") {
                pkgs.push(pkg.name.clone())
            }
        }

        for (n, pkg) in repo_pkgs.iter().rev().enumerate() {
            if menu.contains(len - (n + aur_pkgs.len()), "") {
                pkgs.push(pkg.name().to_string())
            }
        }
    }

    if pkgs.is_empty() {
        sprintln!(" there is nothing to do")
    } else {
        config.need_root = true;
        install(config, &pkgs)?;
    }

    Ok(0)
}