paru 0.99.2

Aur helper and pacman wrapper
use crate::config::{Config, NO_CONFIRM};
use crate::{esprintln, sprint, sprintln};

use std::cell::Cell;
use std::collections::HashMap;
use std::io::{stdin, stdout, BufRead, Write};
use std::ops::Range;

use alpm::PackageReason;
use alpm_utils::{DbListExt, Targ};
use anyhow::Result;

#[derive(Debug)]
pub struct NumberMenu<'a> {
    pub in_range: Vec<Range<usize>>,
    pub ex_range: Vec<Range<usize>>,
    pub in_word: Vec<&'a str>,
    pub ex_word: Vec<&'a str>,
}

pub fn split_repo_aur_pkgs<'a>(config: &Config, pkgs: &[&'a str]) -> (Vec<&'a str>, Vec<&'a str>) {
    let mut local = Vec::new();
    let mut aur = Vec::new();

    for &pkg in pkgs {
        if config.alpm.syncdbs().pkg(pkg).is_ok() {
            local.push(pkg);
        } else {
            aur.push(pkg);
        }
    }

    (local, aur)
}

pub fn split_repo_aur_mode<'a>(config: &Config, pkgs: &[&'a str]) -> (Vec<&'a str>, Vec<&'a str>) {
    if config.mode == "aur" {
        (Vec::new(), pkgs.to_vec())
    } else if config.mode == "repo" {
        (pkgs.to_vec(), Vec::new())
    } else {
        split_repo_aur_pkgs(config, pkgs)
    }
}

pub fn split_repo_aur_targets<'a>(
    config: &Config,
    targets: &[Targ<'a>],
) -> (Vec<Targ<'a>>, Vec<Targ<'a>>) {
    let mut local = Vec::new();
    let mut aur = Vec::new();

    for &targ in targets {
        if config.mode == "aur" {
            aur.push(targ);
        } else if config.mode == "repo" {
            local.push(targ);
        } else if let Some(repo) = targ.repo {
            if repo == "aur" {
                aur.push(targ);
            } else {
                local.push(targ);
            }
        } else if config
            .alpm
            .syncdbs()
            .find_target_satisfier(targ.pkg)
            .unwrap()
            .is_some()
            || config
                .alpm
                .syncdbs()
                .filter(|db| targ.repo.is_none() || db.name() == targ.repo.unwrap())
                .any(|db| db.group(targ.pkg).is_ok())
        {
            local.push(targ);
        } else {
            aur.push(targ);
        }
    }

    (local, aur)
}

pub fn ask(config: &Config, question: &str, default: bool) -> bool {
    let action = config.color.action;
    let bold = config.color.bold;
    let yn = if default { "[Y/n]:" } else { "[n/Y]:" };
    sprint!(
        "{} {} {} ",
        action.paint("::"),
        bold.paint(question),
        bold.paint(yn)
    );
    let _ = stdout().lock().flush();
    if config.no_confirm {
        sprintln!();
        return default;
    }
    let stdin = stdin();
    let mut input = String::new();
    let _ = stdin.read_line(&mut input);
    let input = input.to_lowercase();
    let input = input.trim();

    if input == "y" || input == "yes" {
        true
    } else if input == "n" || input == "no" {
        false
    } else {
        default
    }
}

pub fn input(config: &Config, question: &str) -> String {
    let action = config.color.action;
    let bold = config.color.bold;
    sprint!("{} {}", action.paint("::"), bold.paint(question),);
    let _ = stdout().lock().flush();
    if config.no_confirm {
        sprintln!();
        return "".into();
    }
    let stdin = stdin();
    let mut input = String::new();
    let _ = stdin.read_line(&mut input);
    input
}

#[derive(Hash, PartialEq, Eq, SmartDefault, Copy, Clone)]
enum State {
    #[default]
    Remove,
    CheckDeps,
    Keep,
}

pub fn unneded_pkgs<'a>(config: &'a Config, optional: bool) -> Result<Vec<&'a str>> {
    let mut states = HashMap::new();
    let mut remove = Vec::new();
    let db = config.alpm.localdb();

    let mut providers = HashMap::<_, Vec<_>>::new();

    let pkgs = db.pkgs()?.map(|p| p.name()).collect::<Vec<_>>();
    let (_, aur) = split_repo_aur_pkgs(config, &pkgs);

    for pkg in db.pkgs()? {
        providers
            .entry(pkg.name().to_string())
            .or_default()
            .push(pkg.name());
        for dep in pkg.provides() {
            providers
                .entry(dep.name().to_string())
                .or_default()
                .push(pkg.name())
        }

        if pkg.reason() == PackageReason::Explicit {
            states.insert(pkg.name(), Cell::new(State::CheckDeps));
        } else {
            states.insert(pkg.name(), Cell::new(State::Remove));
        }
    }

    let mut again = true;

    while again {
        again = false;

        let mut check_deps = |deps: alpm::AlpmList<alpm::Depend>| {
            for dep in deps {
                if let Some(deps) = providers.get(dep.name()) {
                    for dep in deps {
                        let state = states.get(dep).unwrap();

                        if state.get() != State::Keep {
                            state.set(State::CheckDeps);
                            again = true;
                        }
                    }
                }
            }
        };

        for (&pkg, state) in &states {
            if state.get() != State::CheckDeps {
                continue;
            }

            if let Ok(pkg) = db.pkg(pkg) {
                state.set(State::Keep);
                check_deps(pkg.depends());

                if config.clean > 1 {
                    continue;
                }

                if optional {
                    check_deps(pkg.optdepends());
                }

                if aur.iter().any(|&a| a == pkg.name()) {
                    check_deps(pkg.makedepends());
                    check_deps(pkg.checkdepends());
                }
            }
        }
    }

    for pkg in db.pkgs()? {
        if states.get(pkg.name()).unwrap().get() == State::Remove {
            remove.push(pkg.name());
        }
    }

    Ok(remove)
}

impl<'a> NumberMenu<'a> {
    pub fn new(input: &'a str) -> Self {
        let mut inclue_range = Vec::new();
        let mut exclude_range = Vec::new();
        let mut inclue_repo = Vec::new();
        let mut exclude_repo = Vec::new();

        let words = input
            .split(|c: char| c.is_whitespace() || c == ',')
            .filter(|s| !s.is_empty());

        for mut word in words {
            let mut invert = false;
            if word.starts_with('^') {
                word = word.trim_start_matches('^');
                invert = true;
            }

            let mut split = word.split('-');
            let start_str = split.next().unwrap();

            let start = match start_str.parse::<usize>() {
                Ok(start) => start,
                Err(_) => {
                    if invert {
                        exclude_repo.push(start_str);
                    } else {
                        inclue_repo.push(start_str);
                    }
                    continue;
                }
            };

            let end = match split.next() {
                Some(end) => end,
                None => {
                    if invert {
                        exclude_range.push(start..start + 1);
                    } else {
                        inclue_range.push(start..start + 1);
                    }
                    continue;
                }
            };

            match end.parse::<usize>() {
                Ok(end) => {
                    if invert {
                        exclude_range.push(start..end + 1)
                    } else {
                        inclue_range.push(start..end + 1)
                    }
                }
                _ => {
                    if invert {
                        exclude_repo.push(start_str)
                    } else {
                        inclue_repo.push(start_str)
                    }
                }
            }
        }

        NumberMenu {
            in_range: inclue_range,
            ex_range: exclude_range,
            in_word: inclue_repo,
            ex_word: exclude_repo,
        }
    }

    pub fn contains(&self, n: usize, word: &str) -> bool {
        if self.in_range.iter().any(|r| r.contains(&n)) || self.in_word.contains(&word) {
            true
        } else if self.ex_range.iter().any(|r| r.contains(&n)) || self.ex_word.contains(&word) {
            false
        } else {
            self.in_range.is_empty() && self.in_word.is_empty()
        }
    }
}

pub fn get_provider(max: usize) -> usize {
    let mut input = String::new();

    loop {
        sprint!("\nEnter a number (default=1): ");
        let _ = stdout().lock().flush();
        input.clear();

        if !NO_CONFIRM.get().unwrap() {
            let stdin = stdin();
            let mut stdin = stdin.lock();
            let _ = stdin.read_line(&mut input);
        }

        let num = input.trim();
        if num.is_empty() {
            return 0;
        }

        let num = match num.parse::<usize>() {
            Err(_) => {
                esprintln!("invalid number: {}", num);
                continue;
            }
            Ok(num) => num,
        };

        if num < 1 || num > max {
            esprintln!("invalid value: {} is not between 1 and {}", num, max);
            continue;
        }

        return num - 1;
    }
}