/*
 * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
 *
 * SPDX-License-Identifier: Apache-2.0 OR MIT
 */

use std::collections::{HashMap, HashSet};

#[derive(Debug, Clone)]
pub struct TargetObj {
    pub id: String,
    pub matched: bool,
    pub protected: bool,
    pub may_delete: bool,
    pub parent: Option<String>,
}

fn protected_with_ancestors(objs: &[TargetObj]) -> HashSet<String> {
    let by_id: HashMap<&str, &TargetObj> = objs.iter().map(|o| (o.id.as_str(), o)).collect();
    let mut protected: HashSet<String> = HashSet::new();
    for o in objs {
        if o.protected {
            let mut cur = Some(o.id.clone());
            while let Some(id) = cur {
                if !protected.insert(id.clone()) {
                    break;
                }
                cur = by_id
                    .get(id.as_str())
                    .and_then(|x| x.parent.clone())
                    .filter(|p| by_id.contains_key(p.as_str()));
            }
        }
    }
    protected
}

fn depth(id: &str, by_id: &HashMap<&str, &TargetObj>) -> usize {
    let mut d = 0;
    let mut cur = by_id.get(id).and_then(|o| o.parent.clone());
    let mut seen: HashSet<String> = HashSet::new();
    while let Some(p) = cur {
        if !seen.insert(p.clone()) || !by_id.contains_key(p.as_str()) {
            break;
        }
        d += 1;
        cur = by_id.get(p.as_str()).and_then(|o| o.parent.clone());
    }
    d
}

pub fn candidates(objs: &[TargetObj], tree: bool) -> Vec<String> {
    let protected = protected_with_ancestors(objs);
    let by_id: HashMap<&str, &TargetObj> = objs.iter().map(|o| (o.id.as_str(), o)).collect();
    let mut out: Vec<&TargetObj> = objs
        .iter()
        .filter(|o| !o.matched && o.may_delete && !protected.contains(&o.id))
        .collect();
    if tree {
        out.sort_by(|a, b| {
            depth(&b.id, &by_id)
                .cmp(&depth(&a.id, &by_id))
                .then_with(|| a.id.cmp(&b.id))
        });
    } else {
        out.sort_by(|a, b| a.id.cmp(&b.id));
    }
    out.into_iter().map(|o| o.id.clone()).collect()
}

#[cfg(test)]
mod tests {
    use super::*;

    fn o(id: &str, matched: bool, protected: bool, parent: Option<&str>) -> TargetObj {
        TargetObj {
            id: id.to_owned(),
            matched,
            protected,
            may_delete: true,
            parent: parent.map(str::to_owned),
        }
    }

    #[test]
    fn unmatched_unprotected_are_candidates() {
        let objs = vec![
            o("a", true, false, None),
            o("b", false, false, None),
            o("c", false, true, None),
        ];
        assert_eq!(candidates(&objs, false), vec!["b".to_owned()]);
    }

    #[test]
    fn protected_excludes_ancestors() {
        let objs = vec![
            o("root", false, false, None),
            o("mid", false, false, Some("root")),
            o("leaf", false, true, Some("mid")),
        ];
        assert!(candidates(&objs, true).is_empty());
    }

    #[test]
    fn may_delete_false_is_skipped() {
        let mut x = o("x", false, false, None);
        x.may_delete = false;
        assert!(candidates(&[x], false).is_empty());
    }

    #[test]
    fn tree_destroy_is_leaf_first() {
        let objs = vec![
            o("root", false, false, None),
            o("child", false, false, Some("root")),
            o("grand", false, false, Some("child")),
        ];
        assert_eq!(
            candidates(&objs, true),
            vec!["grand".to_owned(), "child".to_owned(), "root".to_owned()]
        );
    }
}
