/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

//! # pathops
//!
//! Figure out optimal ways to visit multiple paths to extract their
//! content ids. For example, for paths `a/b/x` and `a/b/y`, ideally
//! the common prefix `a/b` is only visited once.
//!
//! The main APIs are `CompiledPaths::compile` and `CompiledPaths::execute`.

use std::collections::BTreeMap;
use std::collections::HashMap;
use std::fmt;
use std::sync::Arc;

use anyhow::Result;
use manifest_tree::FileType;
use manifest_tree::Flag;
use manifest_tree::TreeEntry;
use manifest_tree::TreeStore;
use types::HgId;
use types::Key;
use types::PathComponentBuf;
use types::RepoPath;
use types::RepoPathBuf;

/// Compiled instructions about how to visit trees to get content of given paths.
pub struct CompiledPaths {
    ops: Vec<Op>,

    /// Provide next ContentIds.
    content_id_map: HashMap<Vec<Option<(HgId, Flag)>>, ContentId>,

    /// Cache to speed up tree lookup.
    lookup_cache: LookupCache,

    /// Ignore file content changes if the value is true, this is used
    /// for tracing file renames. Default value is `false`.
    ignore_file_content: bool,
}

// The usize is a pointer to `PathComponentBuf` in `Op`.
// Using `usize` makes borrowck happy.
// This is sound since `ops` in `CompiledPaths` is immutable after being constructed.
type LookupCache = HashMap<(HgId, usize), Option<(HgId, Flag)>>;

/// A cheap way of comparing `paths`.
#[derive(Debug, Copy, Clone, Eq, Ord, PartialEq, PartialOrd)]
pub struct ContentId(usize);

/// State machine used to calculate the "ContentId" from a "Root tree"
/// following "Op"s (operations).
struct State<'a> {
    items: Vec<Option<TreeItem<'a>>>,
    output: Vec<Option<(HgId, Flag)>>,
}

/// An item of a tree. It optionally contains a resolved `TreeEntry`.
struct TreeItem<'a> {
    id: HgId,
    flag: Flag,
    path: &'a RepoPath,
    loaded: Option<TreeEntry>,
}

/// Operation that can be applied to `State`.
#[derive(Debug)]
enum Op {
    /// Push the given indexes from the items.
    Output(Vec<usize>),

    /// Look up multiple items. Replace `items` in `State` with lookup result.
    ///
    /// See the `LookupKey`.
    Lookup(Vec<LookupKey>),
}

/// Used in `Op::Lookup`.
struct LookupKey {
    /// Parent tree offset in the `items` vector.
    item_index: usize,

    /// Name to lookup.
    component: PathComponentBuf,

    /// The "redundant" full_path is used as a reference in `TreeItem.path`
    /// to avoid allocation.
    full_path: RepoPathBuf,
}

impl<'a> State<'a> {
    /// Construct `State` from a root tree.
    fn from_root_tree_id(tree_id: HgId) -> Self {
        static ROOT_PATH: RepoPathBuf = RepoPathBuf::new();
        let item = TreeItem {
            id: tree_id,
            flag: Flag::Directory,
            path: &ROOT_PATH,
            loaded: None,
        };
        State {
            items: vec![Some(item)],
            output: Vec::new(),
        }
    }

    /// Convert to output.
    fn into_output(self) -> Vec<Option<(HgId, Flag)>> {
        if let Some(Some(item)) = self.items.get(0) {
            tracing::trace!("   root tree {} => {:?}", item.id, &self.output);
        }
        self.output
    }

    /// Execute an operation.
    fn execute(
        &mut self,
        op: &'a Op,
        tree_store: &dyn TreeStore,
        lookup_cache: &mut LookupCache,
    ) -> Result<()> {
        match op {
            Op::Output(indexes) => {
                for &i in indexes {
                    if let Some(item) = self.items.get(i) {
                        let item: Option<&TreeItem> = item.as_ref();
                        let output: Option<(HgId, Flag)> = item.map(|f| (f.id, f.flag));
                        self.output.push(output);
                    }
                }
            }
            Op::Lookup(keys) => {
                let mut new_items = Vec::with_capacity(keys.len());
                for key in keys {
                    let new_item = match &self.items[key.item_index] {
                        None => None,
                        Some(item) => {
                            let cache_key =
                                (item.id, &key.component as *const PathComponentBuf as usize);
                            let opt_id_flag = if let Some(value) = lookup_cache.get(&cache_key) {
                                *value
                            } else {
                                let tree_entry =
                                    self.load_tree_entry(tree_store, key.item_index)?;
                                let value = match tree_entry.as_ref() {
                                    Some(tree_entry) => {
                                        tree_entry.elements().lookup(&key.component)?
                                    }
                                    None => None,
                                };
                                lookup_cache.insert(cache_key, value);
                                value
                            };
                            opt_id_flag.map(|(id, flag)| TreeItem {
                                id,
                                flag,
                                path: key.full_path.as_ref(),
                                loaded: None,
                            })
                        }
                    };
                    new_items.push(new_item);
                }
                self.items = new_items;
            }
        }
        Ok(())
    }

    /// Try to load a tree from the tree store.
    fn load_tree_entry(
        &mut self,
        tree_store: &dyn TreeStore,
        item_index: usize,
    ) -> Result<Option<TreeEntry>> {
        let item = match self.items.get_mut(item_index) {
            None | Some(None) => return Ok(None),
            Some(Some(item)) => item,
        };
        if item.loaded.is_none() {
            let entry = tree_store.get(item.path, item.id)?;
            let format = tree_store.format();
            item.loaded = Some(TreeEntry(entry, format));
        }
        Ok(Some(item.loaded.as_ref().unwrap().clone()))
    }

    /// Append trees that need to be prefetched to `keys`.
    fn push_prefetch_trees(&self, keys: &mut Vec<Key>) {
        for item in self.items.iter() {
            let item = match item.as_ref() {
                Some(item) => item,
                None => continue,
            };
            if item.flag == Flag::Directory && item.loaded.is_none() {
                keys.push(Key::new(item.path.to_owned(), item.id));
            }
        }
    }
}

impl CompiledPaths {
    /// Constructs a list of `op`s from a list of paths.
    pub fn compile(mut paths: Vec<RepoPathBuf>) -> Self {
        paths.sort_unstable();
        paths.dedup();

        #[derive(Default)]
        struct Tree {
            full_path: RepoPathBuf,
            entries: BTreeMap<PathComponentBuf, Tree>,
            should_output: bool,
        }

        // Step 1: Store the paths in a virtual tree.
        let mut root = Tree::default();
        for path in &paths {
            let mut tree = &mut root;
            let mut full_path = RepoPathBuf::default();
            for component in path.components() {
                full_path.push(component);
                let subtree = tree
                    .entries
                    .entry(component.to_owned())
                    .or_insert_with(|| Tree {
                        full_path: full_path.clone(),
                        ..Default::default()
                    });
                tree = subtree;
            }
            tree.should_output = true;
        }

        // Step 2: BFS the tree and genearte `ops`.
        let mut ops = Vec::new();
        // Trees to visit (within a same depth).
        let mut to_visit = vec![&root];
        // Map from path to `items` index.
        let mut path_indexes: HashMap<RepoPathBuf, usize> =
            std::iter::once((RepoPathBuf::default(), 0)).collect();

        if root.should_output {
            ops.push(Op::Output(vec![0]));
        }

        while !to_visit.is_empty() {
            let mut next_to_visit = Vec::new();
            let mut lookup_keys = Vec::new();
            let mut output_indexes = Vec::new();
            // Prepare Lookup and next_to_visit.
            for tree in &to_visit {
                let item_index = path_indexes[&tree.full_path];
                for (name, subtree) in &tree.entries {
                    if subtree.should_output {
                        output_indexes.push(lookup_keys.len());
                    }
                    lookup_keys.push(LookupKey {
                        item_index,
                        component: name.to_owned(),
                        full_path: subtree.full_path.clone(),
                    });
                    next_to_visit.push(subtree);
                }
            }
            // "Dry-run" the Lookup.
            path_indexes.clear();
            for (i, key) in lookup_keys.iter().enumerate() {
                path_indexes.insert(key.full_path.clone(), i);
            }
            // Append the lookup keys.
            if !lookup_keys.is_empty() {
                ops.push(Op::Lookup(lookup_keys));
                if !output_indexes.is_empty() {
                    ops.push(Op::Output(output_indexes));
                }
            }
            to_visit = next_to_visit;
        }

        tracing::debug!(" compiled pathops: {:?}", &ops);

        Self {
            ops,
            content_id_map: Default::default(),
            lookup_cache: Default::default(),
            ignore_file_content: Default::default(),
        }
    }

    /// Set the value of ignore_file_content flag.
    ///
    /// When enabled, file content and file type (e.g. executable) will be ignored,
    /// this is used for tracing renames.
    pub fn with_ignore_file_content(mut self, ignore_file_content: bool) -> Self {
        self.ignore_file_content = ignore_file_content;
        self
    }

    /// Convert a list of root tree ids to `ContentId`s that can be compared.
    pub async fn execute(
        &mut self,
        root_tree_ids: Vec<HgId>,
        tree_store: Arc<dyn TreeStore + Send + Sync>,
    ) -> Result<Vec<ContentId>> {
        let mut states: Vec<State> = root_tree_ids
            .iter()
            .map(|id| State::from_root_tree_id(*id))
            .collect();

        for op in &self.ops {
            // Prefetch before "Lookup".
            if let Op::Lookup(keys) = op {
                let mut prefetch_keys = Vec::with_capacity(keys.len() * states.len());
                for state in states.iter() {
                    state.push_prefetch_trees(&mut prefetch_keys);
                }
                if !prefetch_keys.is_empty() {
                    prefetch_keys.sort_unstable();
                    prefetch_keys.dedup();
                    let tree_store = tree_store.clone();
                    async_runtime::spawn_blocking(move || tree_store.prefetch(prefetch_keys))
                        .await??;
                }
            }
            // Execute the operation.
            for state in states.iter_mut() {
                state.execute(op, &*tree_store, &mut self.lookup_cache)?;
            }
        }

        let mut result = Vec::with_capacity(root_tree_ids.len());
        for state in states {
            let mut output = state.into_output();
            if self.ignore_file_content {
                for o in output.iter_mut() {
                    normalize_content_key_ignore_file_content(o);
                }
            }
            let content_id = if output.iter().all(|o| o.is_none()) {
                ContentId::empty()
            } else {
                let next_content_id = ContentId(self.content_id_map.len() + 1);
                *self.content_id_map.entry(output).or_insert(next_content_id)
            };
            result.push(content_id);
        }
        Ok(result)
    }
}

fn normalize_content_key_ignore_file_content(optional_key: &mut Option<(HgId, Flag)>) {
    if let Some(key) = optional_key.as_mut() {
        // replaces `HgId` to a fixed value (WDIR_ID) to ignore file content changes
        key.0 = *HgId::wdir_id();
        // normalizes executable files as regular files to ignore file type changes
        if let Flag::File(_) = key.1 {
            key.1 = Flag::File(FileType::Regular);
        }
    }
}

impl ContentId {
    /// Empty `ContentId`: all paths are missing.
    pub fn empty() -> Self {
        Self(0)
    }

    /// Test if the `ContentId` is empty.
    pub fn is_empty(&self) -> bool {
        self.0 == 0
    }
}

// Customized Debug impl that is a bit shorter.
impl fmt::Debug for LookupKey {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let full_path = self.full_path.as_str();
        let component = self.component.as_str();
        write!(
            f,
            "{}:{}<{}>",
            self.item_index,
            &full_path[..(full_path.len() - component.len())],
            component,
        )
    }
}

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

    #[test]
    fn test_op_compile() {
        let t = |v: &[&str]| {
            CompiledPaths::compile(v.iter().map(|s| p(s)).collect::<Vec<_>>())
                .ops
                .into_iter()
                .map(|s| format!("{:?}", s))
                .collect::<Vec<String>>()
        };

        // Single path
        assert_eq!(t(&[]), [] as [&str; 0]);
        assert_eq!(t(&[""]), ["Output([0])"]);
        assert_eq!(t(&["a"]), ["Lookup([0:<a>])", "Output([0])"]);
        assert_eq!(
            t(&["a/b/c"]),
            [
                "Lookup([0:<a>])",
                "Lookup([0:a/<b>])",
                "Lookup([0:a/b/<c>])",
                "Output([0])"
            ]
        );

        // Multiple paths
        assert_eq!(
            t(&["a/b/c", "a"]),
            [
                "Lookup([0:<a>])",
                "Output([0])",
                "Lookup([0:a/<b>])",
                "Lookup([0:a/b/<c>])",
                "Output([0])"
            ]
        );
        assert_eq!(
            t(&["a/b/c/d", "a/b/c/e"]),
            [
                "Lookup([0:<a>])",
                "Lookup([0:a/<b>])",
                "Lookup([0:a/b/<c>])",
                "Lookup([0:a/b/c/<d>, 0:a/b/c/<e>])",
                "Output([0, 1])"
            ]
        );
        assert_eq!(
            t(&["a/b/c", "a/d/e", "x/y"]),
            [
                "Lookup([0:<a>, 0:<x>])",
                "Lookup([0:a/<b>, 0:a/<d>, 1:x/<y>])",
                "Output([2])",
                "Lookup([0:a/b/<c>, 1:a/d/<e>])",
                "Output([0, 1])"
            ]
        );
        assert_eq!(
            t(&["a/b", "d", "e/f/g/h", "x/y"]),
            [
                "Lookup([0:<a>, 0:<d>, 0:<e>, 0:<x>])",
                "Output([1])",
                "Lookup([0:a/<b>, 2:e/<f>, 3:x/<y>])",
                "Output([0, 2])",
                "Lookup([1:e/f/<g>])",
                "Lookup([0:e/f/g/<h>])",
                "Output([0])"
            ]
        );
    }

    fn p(s: &str) -> RepoPathBuf {
        RepoPathBuf::from_string(s.to_string()).unwrap()
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use std::sync::Arc;

use anyhow::Result;
use dag::Set;
use dag::Vertex;
use manifest_tree::TreeStore;
use storemodel::ReadRootTreeIds;
use types::RepoPathBuf;

use crate::pathhistory::PathHistoryStats;
use crate::PathHistory;

/// State for answering rename questions about path.
pub struct RenameTracer {
    // RenameTracer is a thin wrapper of PathHistory
    path_history: PathHistory,
}

impl RenameTracer {
    /// RenameTracer is a tool for finding the vertex that added (or renamed to)
    /// the specified path. We will use this in the copy tracing component to trace
    /// a path's rename history.
    pub async fn new(
        set: Set,
        path: RepoPathBuf,
        root_tree_reader: Arc<dyn ReadRootTreeIds + Send + Sync>,
        tree_store: Arc<dyn TreeStore + Send + Sync>,
    ) -> Result<Self> {
        let paths = vec![path];
        let ignore_file_content = true;
        let path_history = PathHistory::new_internal(
            set,
            paths,
            root_tree_reader,
            tree_store,
            ignore_file_content,
        )
        .await?;

        Ok(Self { path_history })
    }

    // Obtain statistics for performance analysis.
    pub fn stats(&self) -> &PathHistoryStats {
        self.path_history.stats()
    }

    /// Find the next vertex that renamed the specified path
    pub async fn next(&mut self) -> Result<Option<Vertex>> {
        self.path_history.next().await
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

//! Testing.

use std::collections::BTreeMap;
use std::collections::HashSet;
use std::sync::Arc;
use std::sync::Mutex;

use anyhow::bail;
use anyhow::Result;
use async_trait::async_trait;
use dag::ops::DagAddHeads;
use dag::ops::DagAlgorithm;
use dag::MemDag;
use dag::NameSet;
use dag::Vertex;
use manifest::FileMetadata;
use manifest::FileType;
use manifest::Manifest;
use manifest_tree::TreeManifest;
use storemodel::minibytes::Bytes;
use storemodel::ReadRootTreeIds;
use storemodel::TreeFormat;
use storemodel::TreeStore;
use types::HgId;
use types::Key;
use types::RepoPath;
use types::RepoPathBuf;

use crate::PathHistory;
use crate::RenameTracer;

#[derive(Clone, Default)]
pub struct TestHistory {
    inner: Arc<Mutex<TestHistoryInner>>,
}

#[derive(Default)]
struct TestHistoryInner {
    /// Commits that change trees.
    commit_to_tree: BTreeMap<u64, HgId>,
    /// Tree store in git format.
    trees: BTreeMap<HgId, Bytes>,
    /// Override commit parents. By default commit x has parent [x-1], commit 0 has no parents.
    commit_parents: BTreeMap<u64, Vec<u64>>,
    /// Empty tree id.
    empty_tree_id: HgId,
    /// Prefetched trees.
    prefetched_trees: HashSet<Key>,
    /// Prefetch logs.
    access_log: Vec<String>,
}

impl TestHistory {
    /// Construct history with changes in this format:
    /// `(commit_index, path, file_content_int, file_type)`.
    /// `commit_index` is an integer that will be translated to a commit hash.
    /// By default, the commit graph is fully linear and 0 is the root commit.
    /// `file_content_int` decides a fake content hash. 0 is special and means deletion.
    ///
    /// Changes are applied in commit order, and are accumulated.
    pub fn from_history(commit_path_content: &[(u64, &'static str, u64, FileType)]) -> Self {
        let this = Self::default();
        {
            let inner = this.inner.lock().unwrap();
            assert!(
                inner.commit_to_tree.is_empty(),
                "history can only be inserted once"
            );
        }

        let mut input = commit_path_content.to_vec();
        input.sort();

        let mut last_commit_int = 0;
        let mut tree = TreeManifest::ephemeral(Arc::new(this.clone()));
        // Write empty tree.
        let empty_tree_id = tree.flush().unwrap();
        for (commit_int, path, content_int, file_type) in input {
            if commit_int > last_commit_int {
                // Commit last_commit.
                let tree_id = tree.flush().unwrap();
                let mut inner = this.inner.lock().unwrap();
                inner.commit_to_tree.insert(last_commit_int, tree_id);
                last_commit_int = commit_int;
            }
            let path = RepoPath::from_str(path).unwrap().to_owned();
            if content_int == 0 {
                tree.remove(&path).unwrap();
            } else {
                let meta = FileMetadata {
                    hgid: hgid_from_int(content_int),
                    file_type,
                };
                tree.insert(path, meta).unwrap();
            }
        }

        let tree_id = tree.flush().unwrap();
        {
            let mut inner = this.inner.lock().unwrap();
            inner.commit_to_tree.insert(last_commit_int, tree_id);
            inner.empty_tree_id = empty_tree_id;
        }

        this
    }

    /// Override parents. By default, the graph is fully linear. This allows
    /// creating non-linear graphs.
    pub fn set_commit_parents(&self, commit_id: u64, parent_ids: &[u64]) -> &Self {
        let mut inner = self.inner.lock().unwrap();
        inner.commit_parents.insert(commit_id, parent_ids.to_vec());
        self
    }

    /// Obtain the `PathHistory` struct.
    pub async fn paths_history(
        &self,
        params: impl Into<BuildSetParam>,
        paths: &[&str],
    ) -> PathHistory {
        // Build commit graph and the "set".
        let set = self.build_set(params.into()).await;

        // Convert path types.
        let paths: Vec<RepoPathBuf> = paths
            .iter()
            .map(|s| RepoPath::from_str(s).unwrap().to_owned())
            .collect();

        let path_history =
            PathHistory::new(set, paths, Arc::new(self.clone()), Arc::new(self.clone()))
                .await
                .unwrap();
        path_history
    }

    /// Obtain the access log.
    pub fn take_access_log(&self) -> Vec<String> {
        self.inner.lock().unwrap().access_log.drain(..).collect()
    }

    fn commit_to_tree(&self, commit_id: HgId) -> HgId {
        let commit_int = hgid_to_int(commit_id);
        let inner = self.inner.lock().unwrap();
        match inner.commit_to_tree.range(..=commit_int).rev().next() {
            Some((_, tree_id)) => *tree_id,
            None => inner.empty_tree_id,
        }
    }

    async fn build_set(&self, build_set_params: BuildSetParam) -> NameSet {
        let BuildSetParam(max_commit_int, selected_commit_ints) = build_set_params;
        // Build commit graph and the "set".
        let mut dag = MemDag::new();
        let parents_override = {
            let inner = self.inner.lock().unwrap();
            inner.commit_parents.clone()
        };
        let parents = move |v: Vertex| -> dag::Result<Vec<Vertex>> {
            let i = vertex_to_int(v);
            let mut ps = Vec::new();
            match parents_override.get(&i) {
                None => {
                    if i > 0 {
                        ps.push(vertex_from_int(i - 1));
                    }
                }
                Some(ids) => {
                    for &p in ids {
                        ps.push(vertex_from_int(p));
                    }
                }
            }
            Ok(ps)
        };
        let parents: Box<dyn Fn(Vertex) -> dag::Result<Vec<Vertex>> + Send + Sync> =
            Box::new(parents);
        for i in 0..=max_commit_int {
            let head = vertex_from_int(i);
            let heads = vec![head.clone()];
            dag.add_heads(&parents, &heads.into()).await.unwrap();
        }

        match selected_commit_ints {
            None => dag.all().await.unwrap(),
            Some(ints) => {
                let set = NameSet::from_static_names(ints.into_iter().map(vertex_from_int));
                dag.sort(&set).await.unwrap()
            }
        }
    }

    // Obtain the `RenameTracer` struct.
    pub async fn rename_tracer(
        &self,
        params: impl Into<BuildSetParam>,
        path: &str,
    ) -> RenameTracer {
        let set = self.build_set(params.into()).await;
        let path = RepoPath::from_str(path).unwrap().to_owned();
        RenameTracer::new(set, path, Arc::new(self.clone()), Arc::new(self.clone()))
            .await
            .unwrap()
    }
}

/// Used by `build_set`. Specifies the graph size and the subset passed to PathHistory.
pub struct BuildSetParam(u64, Option<Vec<u64>>);

impl From<u64> for BuildSetParam {
    fn from(value: u64) -> Self {
        Self(value, None)
    }
}

impl<I: Iterator<Item = u64>> From<(u64, I)> for BuildSetParam {
    fn from(value: (u64, I)) -> Self {
        Self(value.0, Some(value.1.collect()))
    }
}

impl TreeStore for TestHistory {
    fn get(&self, path: &RepoPath, hgid: HgId) -> Result<Bytes> {
        let key = Key::new(path.to_owned(), hgid);
        let inner = self.inner.lock().unwrap();
        if !inner.prefetched_trees.contains(&key) {
            bail!("not prefetched: {:?}", &key);
        }
        match inner.trees.get(&hgid) {
            Some(v) => Ok(v.clone()),
            None => bail!("{:?} not found", &key),
        }
    }

    fn insert(&self, _path: &RepoPath, hgid: HgId, data: Bytes) -> Result<()> {
        self.inner.lock().unwrap().trees.insert(hgid, data);
        Ok(())
    }

    fn prefetch(&self, mut keys: Vec<Key>) -> Result<()> {
        keys.sort();
        let log = keys
            .iter()
            .map(|k| format!("{}/{}", &k.hgid.to_hex()[..5], k.path.as_str()))
            .collect::<Vec<_>>()
            .join(", ");
        let log = format!("Trees: [{}]", log);
        let mut inner = self.inner.lock().unwrap();
        for key in keys {
            inner.prefetched_trees.insert(key);
        }
        inner.access_log.push(log);
        Ok(())
    }

    fn format(&self) -> TreeFormat {
        TreeFormat::Git
    }
}

#[async_trait]
impl ReadRootTreeIds for TestHistory {
    async fn read_root_tree_ids(&self, commits: Vec<HgId>) -> anyhow::Result<Vec<(HgId, HgId)>> {
        let log = commits
            .iter()
            .map(|id| hgid_to_int(*id).to_string())
            .collect::<Vec<_>>()
            .join(", ");
        let log = format!("Commits: [{}]", log);
        let result = commits
            .into_iter()
            .map(|commit_id| {
                let tree_id = self.commit_to_tree(commit_id);
                (commit_id, tree_id)
            })
            .collect();
        self.inner.lock().unwrap().access_log.push(log);
        Ok(result)
    }
}

fn hgid_from_int(v: u64) -> HgId {
    let mut bytes = v.to_le_bytes().to_vec();
    bytes.extend_from_slice(&[0; HgId::len() - 8]);
    HgId::from_slice(&bytes).unwrap()
}

fn hgid_to_int(id: HgId) -> u64 {
    let bytes = &id.as_ref()[0..8];
    let bytes: [u8; 8] = bytes.try_into().unwrap();
    u64::from_le_bytes(bytes)
}

fn vertex_from_int(v: u64) -> Vertex {
    let id = hgid_from_int(v);
    Vertex::copy_from(id.as_ref())
}

fn vertex_to_int(v: Vertex) -> u64 {
    let id = HgId::from_slice(v.as_ref()).unwrap();
    hgid_to_int(id)
}

impl PathHistory {
    async fn next_n(&mut self, mut n: usize) -> Vec<u64> {
        let mut result = Vec::new();
        while let Some(v) = self.next().await.unwrap() {
            result.push(vertex_to_int(v));
            n -= 1;
            if n == 0 {
                break;
            }
        }
        result
    }
}

impl RenameTracer {
    async fn next_n(&mut self, mut n: usize) -> Vec<u64> {
        let mut result = Vec::new();
        while let Some(v) = self.next().await.unwrap() {
            result.push(vertex_to_int(v));
            n -= 1;
            if n == 0 {
                break;
            }
        }
        result
    }
}

// Tests

use FileType::Executable as E;
use FileType::Regular as R;
use FileType::Symlink as S;

#[tokio::test]
async fn test_log_files() {
    let t = TestHistory::from_history(&[
        (0, "a", 1, R),
        (100, "a", 0, R),
        (150, "b", 4, R),
        (200, "a", 3, E),
        (250, "b", 5, E),
    ]);

    let mut h = t.paths_history(300, &["a"]).await;
    assert_eq!(h.next_n(3).await, [200, 100, 0]);

    let _ = t.take_access_log();
    let mut h = t.paths_history(199, &["a"]).await;
    assert_eq!(h.next_n(1).await, [100]);
    assert_eq!(
        t.take_access_log(),
        [
            "Commits: [15, 31, 47, 63, 79, 95, 111, 127, 143, 159, 175, 191, 192, 193, 194, 195, 196, 197, 198, 199, 0]",
            "Trees: [0841a/, 4b825/, 7ba4c/]",
            "Commits: [96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110]",
            "Trees: [0841a/, 4b825/]"
        ]
    );

    let mut h = t.paths_history(250, &["b"]).await;
    assert_eq!(h.next_n(3).await, [250, 150]);

    let mut h = t.paths_history(300, &["a", "b"]).await;
    assert_eq!(h.next_n(10).await, [250, 200, 150, 100, 0]);
}

#[tokio::test]
async fn test_log_dirs() {
    let t = TestHistory::from_history(&[
        (0, "a/b/c/d", 1, R),
        (100, "a/b/c/e", 2, R),
        (150, "a/b/c/d", 0, R),
        (150, "a/b/c/e", 0, R),
        (200, "h/i/j/k", 5, E),
        (250, "a/b/c/f", 3, R),
    ]);

    let mut h = t.paths_history(300, &["a"]).await;
    assert_eq!(h.next_n(5).await, [250, 150, 100, 0]);

    let mut h = t.paths_history(300, &["a/b"]).await;
    assert_eq!(h.next_n(5).await, [250, 150, 100, 0]);

    let mut h = t.paths_history(300, &["a/b/c"]).await;
    assert_eq!(h.next_n(5).await, [250, 150, 100, 0]);

    // answers "who deletes" query too
    let mut h = t.paths_history(300, &["a/b/c/d"]).await;
    assert_eq!(h.next_n(5).await, [150, 0]);

    let mut h = t.paths_history(300, &["a/b/c/d", "h"]).await;
    assert_eq!(h.next_n(5).await, [200, 150, 0]);

    // Check tree prefetch behavior.
    let _ = t.take_access_log();
    let mut h = t.paths_history(300, &["a/b", "h/i"]).await;
    assert_eq!(h.next_n(5).await, [250, 200, 150, 100, 0]);
    assert_eq!(
        t.take_access_log(),
        [
            "Commits: [255, 271, 287, 288, 289, 290, 291, 292, 293, 294, 295, 296, 297, 298, 299, 300, 0]",
            "Trees: [21f04/, d8714/]",
            "Trees: [10831/a, 6f98e/a, 4429c/h]",
            "Commits: [15, 31, 47, 63, 79, 95, 111, 127, 143, 159, 175, 191, 207, 223, 239]",
            "Trees: [21f04/, 4b825/, 991a1/, e25ad/]",
            "Trees: [10831/a, 8b8c5/a, 4429c/h]",
            "Commits: [240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254]",
            "Trees: [991a1/, d8714/]",
            "Trees: [6f98e/a, 4429c/h]",
            "Commits: [192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206]",
            "Trees: [4b825/, 991a1/]",
            "Trees: [4429c/h]",
            "Commits: [144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158]",
            "Trees: [4b825/, e25ad/]",
            "Trees: [8b8c5/a]",
            "Commits: [96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110]",
            "Trees: [21f04/, e25ad/]",
            "Trees: [10831/a, 8b8c5/a]",
            "Commits: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]",
            "Trees: [21f04/]",
            "Trees: [10831/a]"
        ]
    );
}

#[tokio::test]
async fn test_log_with_roots() {
    // Use a commit graph with a few roots.
    let t = TestHistory::from_history(&[(0, "a", 1, R)]);
    t.set_commit_parents(10, &[]);
    t.set_commit_parents(11, &[10, 9]);
    t.set_commit_parents(90, &[]);
    t.set_commit_parents(121, &[89, 120]);

    // Roots are outputted, despite there are no changes.
    let mut h = t.paths_history(300, &["a"]).await;
    assert_eq!(h.next_n(5).await, [90, 10, 0]);

    // Roots are not outputted if the path does not exist.
    let mut h = t.paths_history(300, &["b"]).await;
    assert_eq!(h.next_n(5).await, &[] as &[u64]);
}

#[tokio::test]
async fn test_log_merge_same_with_parent() {
    // b--------merge
    //     /
    // a---
    let n = 8;
    for index_a in 1..n {
        for index_b in index_a + 1..n {
            for index_merge in index_b + 1..n {
                for merge_content in [1, 2] {
                    let t = TestHistory::from_history(&[
                        (0, "a", 0, R),
                        (index_b, "a", 1, R),
                        (index_a, "a", 2, R),
                        (index_merge, "a", merge_content, R),
                    ]);
                    t.set_commit_parents(index_a, &[]);
                    t.set_commit_parents(index_b, &[]);
                    t.set_commit_parents(index_merge, &[index_b, index_a]);

                    // Log should not contain the merge.
                    // It is the same as one parent.
                    let mut h = t.paths_history(n, &["a"]).await;
                    assert!(!h.next_n(5).await.contains(&index_merge));

                    // Swap parents. No merge in history too.
                    t.set_commit_parents(index_merge, &[index_a, index_b]);
                    let mut h = t.paths_history(n, &["a"]).await;
                    assert!(!h.next_n(5).await.contains(&index_merge));
                }
            }
        }
    }
}

#[tokio::test]
async fn test_log_muti_heads_in_testing_range() {
    // This test targets the plain "bisect" algorithm that was
    // tried before using segments. It is generally useful
    // for testing correctness.
    //
    // In revset syntax, (heads(TESTING) - right) is non-empty.
    // Some changes might "escape" the TESTING range check.
    //
    // ---low---escape-----------merge---
    //      \                      /
    //       -------------high-----
    //
    // Index-wise, low (right) < escape < high (left).
    // Content-wise, left and right are same on paths.
    // merge contains "escaped" changes from "escape".

    // Range:
    // Step 1: 1------------------------------2
    // Step 2: 1-------------1                2
    // Step 3:               1---3----1
    //         ^      ^      ^   ^    ^       ^
    //         128   96     64   48   32      0
    //             merge  left escape right
    // Graph:  v      v      v   v    v       v
    //         1  ----3------1--------1-------2
    //                 \             /
    //                  ---------3---
    //
    // Run with `LOG=pathhistory=trace` to see trace.
    let t = TestHistory::from_history(&[
        (0, "a", 2, R),
        (32, "a", 1, R), // right (low)
        (48, "a", 3, R), // escape
        (64, "a", 1, R), // left (high)
        (96, "a", 3, R), // merge
        (120, "a", 1, R),
    ]);
    t.set_commit_parents(64, &[32]);
    t.set_commit_parents(96, &[95, 63]);
    t.set_commit_parents(120, &[]);

    // Log should contain the "escape" commit (48).
    let mut h = t.paths_history(128, &["a"]).await;
    assert_eq!(h.next_n(9).await, [120, 48, 32, 0]);
}

#[tokio::test]
async fn test_log_subset_misleading_parents() {
    // a: 0..80 (0), 80..90 (1), 90..100 (0)
    let t = TestHistory::from_history(&[
        (79, "a", 0, R),
        (80, "a", 1, R),
        (90, "a", 0, R),
        (100, "a", 0, R),
    ]);

    // When listing all changes, PathHistory will miss the change, because "a" is consistently
    // missing even if pathhistory bisects once - it still decide to skip the 50-100 range.
    let mut h = t.paths_history((100, 0..=100), &["a"]).await;
    assert_eq!(h.next_n(9).await, &[] as &[u64]);

    // When we pass a subset that is not "ancestors(something)" to PathHistory, it can detect
    // changes in the given range.
    let mut h = t.paths_history((100, 85..=95), &["a"]).await;
    assert_eq!(h.next_n(9).await, &[90]);

    // In this case, we expect PathHistory to not skip the 80 to 95 range by detecting "a" is the
    // same in 79 (parent of 80) and 95.
    let mut h = t.paths_history((100, 80..=95), &["a"]).await;
    assert_eq!(h.next_n(9).await, &[90, 80]);
}

#[tokio::test]
async fn test_log_with_mode_only_changes() {
    let t = TestHistory::from_history(&[(0, "a", 1, R), (100, "a", 1, E), (200, "a", 1, S)]);
    let mut h = t.paths_history(300, &["a"]).await;
    assert_eq!(h.next_n(5).await, [200, 100, 0]);
}

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

    #[tokio::test]
    async fn test_trace_rename_files() {
        let t = TestHistory::from_history(&[
            (0, "a", 1, R),
            (100, "a", 2, R),
            // rename a -> b
            (150, "a", 0, R),
            (150, "b", 4, R),
            (200, "b", 4, R),
            (250, "b", 5, E),
        ]);

        let mut r = t.rename_tracer(250, "b").await;
        assert_eq!(r.next_n(2).await, [150]);

        // commit 150 did the rename (b -> a), so we search commit 149 (150-1)
        // with name "a"
        let mut r = t.rename_tracer(149, "a").await;
        assert_eq!(r.next_n(2).await, [0]);
    }

    #[tokio::test]
    async fn test_trace_nested_rename_files() {
        let t = TestHistory::from_history(&[
            (0, "a/b/1", 1, R),
            (100, "a/b/1", 2, R),
            // rename a/b/1 -> a/2
            (150, "a/b/1", 0, R),
            (150, "a/2", 4, R),
            (200, "a/2", 4, R),
            (250, "a/2", 5, E),
        ]);

        let mut r = t.rename_tracer(250, "a/2").await;
        assert_eq!(r.next_n(2).await, [150]);

        // commit 150 did the rename (b -> a), so we search commit 149 (150-1)
        // with name "a"
        let mut r = t.rename_tracer(149, "a/b/1").await;
        assert_eq!(r.next_n(2).await, [0]);
    }

    #[tokio::test]
    async fn test_trace_commit_added_files() {
        let t = TestHistory::from_history(&[
            (0, "a", 1, R),
            (100, "a", 2, R),
            (150, "b", 3, R),
            (160, "a", 4, R),
            (200, "b", 5, R),
            (210, "b", 6, R),
            (220, "b", 7, R),
            (250, "b", 7, E),
        ]);

        let mut r = t.rename_tracer(250, "a").await;
        assert_eq!(r.next_n(2).await, [0]);

        let mut r = t.rename_tracer(250, "b").await;
        assert_eq!(r.next_n(2).await, [150]);
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

//! Utilities used by the main path history logic.

/// Pick a `mid` value that is between `left` and `right`, but equals to
/// none of them. Try to keep `right - mid` a "fixed" value to increase
/// cache hits (assuming `right` is "root" of a complete dag).
/// Return `None` if there is no such `mid`.
pub(crate) fn pick_mid(left: u64, right: u64) -> Option<u64> {
    if left + 1 >= right {
        return None;
    }
    // Only keep the higest 2 bits of `delta`. Drop other bits
    // (set them to 0). This makes `right - delta` more stable
    // with different `left`s.
    let delta = (right - left + 1) / 2;
    let mask = 0b11u64 << (62u32.saturating_sub(delta.leading_zeros()));
    let mid = right - (delta & mask);
    debug_assert!(mid > left);
    debug_assert!(mid < right);
    Some(mid)
}

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

    #[test]
    fn test_pick_mid() {
        assert_eq!(pick_mid(1, 1), None);
        assert_eq!(pick_mid(10, 11), None);
        assert_eq!(pick_mid(10, 12), Some(11));
        assert_eq!(pick_mid(10, 13), Some(11));
        assert_eq!(pick_mid(10, 15), Some(12));

        let n = 300;
        for left in 0..=n {
            for right in left..=n {
                match pick_mid(left, right) {
                    None => assert!(right - left <= 1),
                    Some(mid) => {
                        assert!(left < mid);
                        assert!(mid < right);
                    }
                }
            }
        }

        // `mid` remains somewhat stable with different `left`s.
        assert_eq!(pick_mid(10, 20000000), Some(11611392));
        assert_eq!(pick_mid(10000, 20000000), Some(11611392));
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

#[derive(Debug, thiserror::Error)]
pub enum Error {
    #[error("unsuppported pattern kind {0}")]
    UnsupportedPatternKind(String),

    #[error(transparent)]
    IOError(#[from] util::errors::IOError),
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

//! Pattern matcher that matches an exact set of paths.

use std::borrow::Cow;
use std::collections::HashMap;

use anyhow::Result;
use types::RepoPath;

use crate::DirectoryMatch;
use crate::Matcher;

/// A [Matcher] that only matches an exact list of file paths.
#[derive(Clone, Debug)]
pub struct ExactMatcher {
    root: Node,
}

impl ExactMatcher {
    /// Create [ExactMatcher] using an exact list of file paths.
    ///
    /// The matcher will only match files explicitly listed.
    pub fn new(paths: impl Iterator<Item = impl AsRef<RepoPath>>, case_sensitive: bool) -> Self {
        let mut root = Node::new(case_sensitive);
        for path in paths {
            root.insert(path.as_ref());
        }
        ExactMatcher { root }
    }

    /// Insert a new path into the set of paths matched.
    pub fn add(&mut self, path: &RepoPath) {
        self.root.insert(path);
    }
}

impl Matcher for ExactMatcher {
    fn matches_directory(&self, path: &RepoPath) -> Result<DirectoryMatch> {
        match self.root.find(path) {
            Some(node) if !node.children.is_empty() => Ok(DirectoryMatch::ShouldTraverse),
            _ => Ok(DirectoryMatch::Nothing),
        }
    }

    fn matches_file(&self, path: &RepoPath) -> Result<bool> {
        match self.root.find(path) {
            Some(node) => Ok(node.is_file),
            None => Ok(false),
        }
    }
}

#[derive(Clone, Debug)]
struct Node {
    /// Child nodes (for directories).
    children: HashMap<String, Node>,

    /// Whether this node represents a specific file.
    is_file: bool,

    /// True if it should do case insensitive comparisons.
    case_sensitive: bool,
}

impl Node {
    fn new(case_sensitive: bool) -> Self {
        Node {
            children: HashMap::new(),
            is_file: false,
            case_sensitive,
        }
    }

    /// Find the node corresponding to the given path (rooted at this directory),
    /// or [`None`] if there is no node.
    fn find(&self, path: &RepoPath) -> Option<&Node> {
        let mut node = self;
        let mut components = path.components();
        while let Some(component) = components.next() {
            let component: Cow<str> = if node.case_sensitive {
                Cow::Borrowed(component.as_str())
            } else {
                Cow::Owned(component.as_str().to_lowercase())
            };
            node = node.children.get(component.as_ref())?;
        }
        Some(node)
    }

    /// Insert the given path (rooted at this directory) as a file.
    fn insert(&mut self, path: &RepoPath) {
        let mut node = self;

        let mut components = path.components().peekable();
        while let Some(component) = components.next() {
            let component = if node.case_sensitive {
                component.as_str().to_string()
            } else {
                component.as_str().to_lowercase()
            };
            let entry = node.children.entry(component);
            let new_node = entry.or_insert_with(|| Node::new(node.case_sensitive));
            // If this is the final path component, then this component represents a file.
            let is_file = components.peek().is_none();

            if is_file {
                new_node.is_file = true;
                break;
            } else {
                node = new_node;
            }
        }
    }
}

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

    #[test]
    fn test_basic() {
        let paths = ["file1", "d1/d2", "d1/d2/file2", "d1/file3", "d2/file4"];
        let paths = paths
            .iter()
            .map(|p| RepoPath::from_str(p).unwrap().to_owned());
        let m = ExactMatcher::new(paths, true);

        // Test regular file matching.
        let cases = [
            ("", false), // empty path shouldn't match
            ("file1", true),
            ("d1/d2/file2", true),
            ("d1/file3", true),
            ("d2/file4", true),
            ("bad_file", false),
            ("bad_dir/f3", false),
            ("d1/bad", false),
            ("d1/d2/bad", false),
            ("d1", false),   // regular directories shouldn't match
            ("d1/d2", true), // directories that are also files should match
            ("d1/d2/file", false),
            ("file", false), // name prefixes shouldn't match
        ];
        for (path, should_match) in cases {
            let matches = m.matches_file(RepoPath::from_str(path).unwrap()).unwrap();
            assert_eq!(should_match, matches, "Matching {:?}", path);
        }

        // Test directory prefix lookups.
        use DirectoryMatch::*;
        let cases = [
            ("", ShouldTraverse),
            ("d1", ShouldTraverse),
            ("d1/d2", ShouldTraverse),
            ("d1/d2/d3", Nothing),
            ("d1/fake2", Nothing),
            ("fake1", Nothing),
        ];
        for (path, expected) in cases {
            let actual = m
                .matches_directory(RepoPath::from_str(path).unwrap())
                .unwrap();
            assert_eq!(expected, actual, "Directory match {:?}", path);
        }
    }

    #[test]
    fn test_case_insensitive() {
        let paths = ["dir/file"];
        let paths = paths
            .iter()
            .map(|p| RepoPath::from_str(p).unwrap().to_owned());
        let case_sensitive = [true, false];
        for sensitive in case_sensitive {
            let m = ExactMatcher::new(paths.clone(), sensitive);
            let cases = ["dir/file", "DIR/file", "dir/FILE", "DIR/FILE"];
            for path in cases {
                let matches = m.matches_file(RepoPath::from_str(path).unwrap()).unwrap();
                let expected = path == "dir/file" || !sensitive;
                assert_eq!(expected, matches, "Matching {:?}", path);
            }
        }
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use std::collections::HashMap;
use std::path::Component;
use std::path::Path;
use std::path::PathBuf;

use anyhow::Result;
use ignore::gitignore;
use ignore::gitignore::Glob;
use ignore::Match;
use parking_lot::RwLock;
use types::RepoPath;

use crate::DirectoryMatch;
use crate::Matcher;

/// Lazy `.gitignore` matcher that loads `.gitignore` files on demand.
pub struct GitignoreMatcher {
    ignore: gitignore::Gitignore,

    // PERF: Each Gitignore object stores "root" as "PathBuf" to support
    // matching against an absolute path. Since we enforce relative path
    // in the API, removing that "PathBuf" could reduce memory footprint.
    submatchers: RwLock<HashMap<PathBuf, Box<GitignoreMatcher>>>,

    // Whether this directory is ignored or not.
    ignored: bool,

    case_sensitive: bool,
}

/// Return (next_component, remaining_path), or None if remaining_path is empty.
fn split_path(path: &Path) -> Option<(&Path, &Path)> {
    let mut comps = path.components();
    let comp = comps.next();
    comp.and_then(|c| {
        let rest = comps.as_path();
        if let Component::Normal(s) = c {
            if rest.as_os_str().is_empty() {
                None
            } else {
                Some((Path::new(s), rest))
            }
        } else {
            panic!("ProgrammingError: unexpected path component {:?}", &c);
        }
    })
}

#[derive(PartialEq)]
enum MatchResult {
    Unspecified,
    Ignored,
    Included,
}

impl<T> From<ignore::Match<T>> for MatchResult {
    fn from(v: ignore::Match<T>) -> MatchResult {
        match v {
            ignore::Match::None => MatchResult::Unspecified,
            ignore::Match::Ignore(_) => MatchResult::Ignored,
            ignore::Match::Whitelist(_) => MatchResult::Included,
        }
    }
}

impl GitignoreMatcher {
    /// Initialize `GitignoreMatch` for the given root directory.
    ///
    /// The `.gitignore` in the root directory will be parsed immediately.
    /// `.gitignore` in subdirectories are parsed lazily.
    ///
    /// `global_gitignore_paths` is an additional list of gitignore files
    /// to be parsed.
    pub fn new<P: AsRef<Path>>(
        root: P,
        global_gitignore_paths: Vec<&Path>,
        case_sensitive: bool,
    ) -> Self {
        let root = root.as_ref();
        let mut builder = gitignore::GitignoreBuilder::new(root);

        // It's safe to ignore the Result, since it's always Ok().
        let _ = builder.case_insensitive(!case_sensitive);

        for path in global_gitignore_paths {
            builder.add(path);
        }
        builder.add(root.join(".gitignore"));
        let ignore = builder
            .build()
            .unwrap_or_else(|_| gitignore::Gitignore::empty());

        let submatchers = RwLock::new(HashMap::new());
        GitignoreMatcher {
            ignore,
            submatchers,
            ignored: false,
            case_sensitive,
        }
    }

    /// Like `new`, but might mark the subtree as "ignored" entirely.
    /// Used internally by `match_subdir_path`.
    fn new_with_rootmatcher(dir: &Path, root: &GitignoreMatcher) -> Self {
        let dir_root_relative = dir.strip_prefix(root.ignore.path()).unwrap();
        let submatchers = RwLock::new(HashMap::new());
        let (ignored, ignore) = if root.match_relative(dir_root_relative, true) {
            (true, gitignore::Gitignore::empty())
        } else {
            let mut builder = gitignore::GitignoreBuilder::new(dir);
            // It's safe to ignore the Result, since it's always Ok().
            let _ = builder.case_insensitive(!root.case_sensitive);
            builder.add(dir.join(".gitignore"));
            (
                false,
                builder
                    .build()
                    .unwrap_or_else(|_| gitignore::Gitignore::empty()),
            )
        };
        GitignoreMatcher {
            ignore,
            ignored,
            submatchers,
            case_sensitive: root.case_sensitive,
        }
    }

    /// Return true if the normalized relative path should be ignored.
    ///
    /// Panic if the path is not relative, or contains components like
    /// ".." or ".".
    pub fn match_relative<P: AsRef<Path>>(&self, path: P, is_dir: bool) -> bool {
        let path = path.as_ref();
        self.match_path(path, is_dir, self, &mut None) == MatchResult::Ignored
    }

    /// Check .gitignore for the relative path.
    fn match_path<P: AsRef<Path>>(
        &self,
        path: P,
        is_dir: bool,
        root: &GitignoreMatcher,
        explain: &mut Option<&mut Explain>,
    ) -> MatchResult {
        let path = path.as_ref();
        // Everything is ignored regardless if this directory is ignored.
        if self.ignored {
            if let Some(ref mut explain) = explain {
                explain.parent_ignored(path, root);
            }
            return MatchResult::Ignored;
        }

        // If explain information is requested, always check this (parent)
        // directory to explain overridden rules.
        if let Some(ref mut explain) = explain {
            let matched = self.ignore.matched(path, is_dir);
            match matched {
                Match::Ignore(glob) => explain.add_glob(glob),
                Match::Whitelist(glob) => explain.add_glob(glob),
                _ => {}
            }
        }

        // Check subdir first. It can override this (parent) directory.
        let subdir_result = match split_path(path) {
            None => MatchResult::Unspecified,
            Some((dir, rest)) => self.match_subdir_path(dir, rest, is_dir, root, explain),
        };

        match subdir_result {
            MatchResult::Included => MatchResult::Included,
            MatchResult::Ignored => MatchResult::Ignored,
            MatchResult::Unspecified => self.ignore.matched(path, is_dir).into(),
        }
    }

    /// Check .gitignore in the subdirectory `name` for the path `rest`.
    /// Create submatcher on demand.
    fn match_subdir_path(
        &self,
        name: &Path,
        rest: &Path,
        is_dir: bool,
        root: &GitignoreMatcher,
        explain: &mut Option<&mut Explain>,
    ) -> MatchResult {
        {
            let submatchers = self.submatchers.read_recursive();
            if let Some(m) = submatchers.get(name) {
                return m.as_ref().match_path(rest, is_dir, root, explain);
            }
        }
        {
            let dir = self.ignore.path().join(name);
            if dir.is_dir() {
                let m = GitignoreMatcher::new_with_rootmatcher(&dir, root);
                let result = m.match_path(rest, is_dir, root, explain);
                let mut submatchers = self.submatchers.write();
                submatchers.insert(name.to_path_buf(), Box::new(m));
                result
            } else {
                MatchResult::Unspecified
            }
        }
    }

    /// Explain why a path is ignored or not included. This includes rules
    /// including and excluding the given path, or why parent directories
    /// are ignored.
    ///
    /// Return human-readable text.
    pub fn explain(&self, path: impl AsRef<Path>, is_dir: bool) -> String {
        let mut explain = Explain::new();
        let path = path.as_ref().to_path_buf();
        explain.start_explain(path.clone(), is_dir, self);
        explain.human_text(path.clone(), self)
    }
}

/// Context related for the "explain" feature.
struct Explain {
    /// Path being currently explained. The "current" input.
    path: PathBuf,

    /// Related rules affecting the glob. The output.
    rules: Vec<(Glob, PathBuf)>,
}

impl Explain {
    fn new() -> Self {
        let path = PathBuf::new();
        let rules = Vec::new();
        Self { path, rules }
    }

    /// Explain why `path` is ignored.
    fn start_explain(&mut self, path: PathBuf, is_dir: bool, root: &GitignoreMatcher) {
        self.path = path.clone();
        root.match_path(&path, is_dir, root, &mut Some(self));
    }

    /// The glob affects whether `self.path` is ignored or not ignored.
    fn add_glob(&mut self, glob: &Glob) {
        self.rules.push((glob.clone(), self.path.clone()));
    }

    /// `self.path` is ignored because a parent directory is ignored.
    fn parent_ignored(&mut self, suffix: &Path, root: &GitignoreMatcher) {
        // self.path (= prefix + suffix) is ignored because prefix is ignored.
        let mut prefix = self.path.clone();
        for _ in 0..suffix.components().count() {
            prefix.pop();
        }
        self.start_explain(prefix, true, root);
    }

    /// Return human readable text.
    fn human_text(&self, path: PathBuf, root: &GitignoreMatcher) -> String {
        let mut text = String::new();
        let mut current_path = path;
        let mut current_count = 0;

        if self.rules.is_empty() {
            text.push_str(&format!("{}: not ignored\n", self.path.to_string_lossy()));
        }

        let get_overrides = |count: usize| {
            if count > 0 {
                " (overrides previous rules)"
            } else {
                ""
            }
        };

        for (glob, path) in self.rules.iter() {
            let action = if glob.is_whitelist() {
                "unignored"
            } else {
                "ignored"
            };

            let from = match glob.from() {
                Some(path) => {
                    let path = path.strip_prefix(root.ignore.path()).unwrap_or(path);
                    format!("from {}", path.to_string_lossy())
                }
                None => format!(""),
            };

            if path != &current_path {
                text.push_str(&format!(
                    "{}: ignored because {} is ignored{}\n",
                    current_path.to_string_lossy(),
                    path.to_string_lossy(),
                    get_overrides(current_count),
                ));
                current_path = path.clone();
                current_count = 0;
            }

            text.push_str(&format!(
                "{}: {} by rule {} {}{}\n",
                path.to_string_lossy(),
                action,
                glob.original(),
                from,
                get_overrides(current_count),
            ));

            current_count += 1;
        }

        text
    }
}

impl Matcher for GitignoreMatcher {
    fn matches_directory(&self, path: &RepoPath) -> Result<DirectoryMatch> {
        let dm = match self.match_path(path.as_str(), true, self, &mut None) {
            MatchResult::Ignored => DirectoryMatch::Everything,
            MatchResult::Included => DirectoryMatch::Nothing,
            MatchResult::Unspecified => DirectoryMatch::ShouldTraverse,
        };
        Ok(dm)
    }

    fn matches_file(&self, path: &RepoPath) -> Result<bool> {
        Ok(self.match_relative(path.as_str(), false))
    }
}

#[cfg(test)]
mod tests {
    use std::fs::create_dir_all;
    use std::fs::File;
    use std::io::Write;

    use tempfile::tempdir;

    use super::*;

    #[test]
    fn test_split_path() {
        let p = Path::new("proc/self/stat");

        let (c, p) = split_path(p).unwrap();
        assert_eq!(c, Path::new("proc"));
        assert_eq!(p, Path::new("self/stat"));

        let (c, p) = split_path(p).unwrap();
        assert_eq!(c, Path::new("self"));
        assert_eq!(p, Path::new("stat"));

        assert!(split_path(p).is_none());
    }

    #[test]
    fn test_gitignore_match_directory() {
        let dir = tempdir().unwrap();
        write(dir.path().join(".gitignore"), b"FILE\nDIR/\n");

        let m = GitignoreMatcher::new(dir.path(), Vec::new(), true);
        assert!(m.match_relative("x/FILE", false));
        assert!(m.match_relative("x/FILE", true));
        assert!(!m.match_relative("x/DIR", false));
        assert!(m.match_relative("x/DIR", true));

        assert_eq!(
            m.explain("x/FILE", true),
            "x/FILE: ignored by rule FILE from .gitignore\n"
        );
        assert_eq!(
            m.explain("x/DIR/bar/baz", true),
            "x/DIR/bar/baz: not ignored\n"
        );
    }

    #[test]
    fn test_gitignore_match_subdir() {
        let dir = tempdir().unwrap();

        create_dir_all(dir.path().join("a/b")).expect("mkdir");
        create_dir_all(dir.path().join("c/d")).expect("mkdir");
        write(dir.path().join(".gitignore"), b"a/b\n!c/d/*");
        write(dir.path().join("a/b/.gitignore"), b"!c");
        write(dir.path().join("a/.gitignore"), b"!b/d");
        write(dir.path().join("c/.gitignore"), b"d/e\n!d/f");
        write(dir.path().join("c/d/.gitignore"), b"!e\nf");

        let m = GitignoreMatcher::new(dir.path(), Vec::new(), true);
        assert!(m.match_relative("a/b", false));
        assert!(m.match_relative("a/b/c", false));
        assert!(m.match_relative("a/b/d", false));
        assert!(m.match_relative("c/d/f", false));
        assert!(!m.match_relative("c/d/e", false));

        assert_eq!(
            m.explain("a/b", false),
            "a/b: ignored by rule a/b from .gitignore\n"
        );
        assert_eq!(
            m.explain("a/b/c", false),
            r#"a/b/c: ignored because a/b is ignored
a/b: ignored by rule a/b from .gitignore
"#
        );

        // Windows uses `\` instead of `/` as path separator
        #[cfg(unix)]
        {
            assert_eq!(
                m.explain("a/b/d", false),
                r#"a/b/d: unignored by rule !b/d from a/.gitignore
a/b/d: ignored because a/b is ignored (overrides previous rules)
a/b: ignored by rule a/b from .gitignore
"#
            );
            assert_eq!(
                m.explain("c/d/f", false),
                r#"c/d/f: unignored by rule !c/d/* from .gitignore
c/d/f: unignored by rule !d/f from c/.gitignore (overrides previous rules)
c/d/f: ignored by rule f from c/d/.gitignore (overrides previous rules)
"#
            );
            assert_eq!(
                m.explain("c/d/e", false),
                r#"c/d/e: unignored by rule !c/d/* from .gitignore
c/d/e: ignored by rule d/e from c/.gitignore (overrides previous rules)
c/d/e: unignored by rule !e from c/d/.gitignore (overrides previous rules)
"#
            );
        }
    }

    #[test]
    fn test_global_gitignore() {
        let dir = tempdir().unwrap();
        let ignore1_path = dir.path().join("ignore1");
        let ignore2_path = dir.path().join("ignore2");

        write(&ignore1_path, b"a*");
        write(&ignore2_path, b"b*");

        let m = GitignoreMatcher::new(dir.path(), vec![&ignore1_path, &ignore2_path], true);
        assert!(m.match_relative("a1", true));
        assert!(m.match_relative("b1", true));

        assert_eq!(
            m.explain("a1", true),
            "a1: ignored by rule a* from ignore1\n"
        );
        assert_eq!(
            m.explain("b1", true),
            "b1: ignored by rule b* from ignore2\n"
        );
    }

    #[test]
    fn test_explain() {
        let dir = tempdir().unwrap();
        create_dir_all(dir.path().join("a/b")).unwrap();
        create_dir_all(dir.path().join("c/d/e")).unwrap();
        create_dir_all(dir.path().join("c/f/g")).unwrap();
        create_dir_all(dir.path().join("c/g")).unwrap();
        write(dir.path().join(".gitignore"), b"*.pyc\nd/\ng/");
        write(dir.path().join("a/.gitignore"), b"!a*.pyc");
        write(dir.path().join("a/b/.gitignore"), b"a1*.pyc");
        write(dir.path().join("c/.gitignore"), b"!g/");
        write(dir.path().join("c/f/.gitignore"), b"g/");

        let m = GitignoreMatcher::new(dir.path(), Vec::new(), true);
        assert_eq!(
            m.explain("1.pyc", true),
            "1.pyc: ignored by rule *.pyc from .gitignore\n"
        );

        // Windows uses `\` instead of `/` as path separator
        #[cfg(unix)]
        {
            assert_eq!(
                m.explain("a/a1.pyc", true),
                r#"a/a1.pyc: ignored by rule *.pyc from .gitignore
a/a1.pyc: unignored by rule !a*.pyc from a/.gitignore (overrides previous rules)
"#
            );
            assert_eq!(
                m.explain("a/b/a10.pyc", true),
                r#"a/b/a10.pyc: ignored by rule *.pyc from .gitignore
a/b/a10.pyc: unignored by rule !a*.pyc from a/.gitignore (overrides previous rules)
a/b/a10.pyc: ignored by rule a1*.pyc from a/b/.gitignore (overrides previous rules)
"#
            );
            assert_eq!(
                m.explain("a/b/a2.pyc", true),
                r#"a/b/a2.pyc: ignored by rule *.pyc from .gitignore
a/b/a2.pyc: unignored by rule !a*.pyc from a/.gitignore (overrides previous rules)
"#
            );
            assert_eq!(m.explain("a/b/a2.py", true), "a/b/a2.py: not ignored\n");

            assert_eq!(
                m.explain("c/d/e/f", true),
                r#"c/d/e/f: ignored because c/d is ignored
c/d: ignored by rule d/ from .gitignore
"#
            );
            assert_eq!(
                m.explain("c/d", true),
                "c/d: ignored by rule d/ from .gitignore\n"
            );
            assert_eq!(m.explain("c/d", false), "c/d: not ignored\n");

            assert_eq!(
                m.explain("c/f/g/1/2", true),
                r#"c/f/g/1/2: ignored because c/f/g is ignored
c/f/g: ignored by rule g/ from .gitignore
c/f/g: unignored by rule !g/ from c/.gitignore (overrides previous rules)
c/f/g: ignored by rule g/ from c/f/.gitignore (overrides previous rules)
"#
            );
        }
        assert_eq!(m.explain("c/g/1/2", true), "c/g/1/2: not ignored\n");

        assert_eq!(m.explain("c/h/1", true), "c/h/1: not ignored\n");
    }

    fn write<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) {
        File::create(path)
            .expect("create")
            .write_all(contents.as_ref())
            .expect("write");
    }

    #[test]
    fn test_case_insensitive() {
        let dir = tempdir().unwrap();
        write(dir.path().join(".gitignore"), b"FILE\nDIR/\n");

        let case_sensitive = [true, false];
        for sensitive in case_sensitive {
            let m = GitignoreMatcher::new(dir.path(), Vec::new(), sensitive);
            assert_eq!(true, m.match_relative("x/FILE", false));
            assert_eq!(!sensitive, m.match_relative("x/file", false));
            assert_eq!(true, m.match_relative("x/FILE", true));
            assert_eq!(!sensitive, m.match_relative("x/file", true));
            assert_eq!(false, m.match_relative("x/DIR", false));
            assert_eq!(false, m.match_relative("x/dir", false));
            assert_eq!(true, m.match_relative("x/DIR", true));
            assert_eq!(!sensitive, m.match_relative("x/dir", true));
        }
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

mod error;
mod exact_matcher;
mod gitignore_matcher;
mod matcher;
mod pattern;
mod regex_matcher;
mod tree_matcher;
mod utils;

use std::ops::Deref;
use std::sync::Arc;

use anyhow::Result;
use types::RepoPath;

pub use crate::error::Error;
pub use crate::exact_matcher::ExactMatcher;
pub use crate::gitignore_matcher::GitignoreMatcher;
pub use crate::matcher::build_matcher;
pub use crate::pattern::build_patterns;
pub use crate::pattern::split_pattern;
pub use crate::pattern::PatternKind;
pub use crate::regex_matcher::RegexMatcher;
pub use crate::tree_matcher::TreeMatcher;
pub use crate::utils::expand_curly_brackets;
pub use crate::utils::normalize_glob;
pub use crate::utils::plain_to_glob;

/// Limits the set of files to be operated on.
pub trait Matcher {
    /// This method is intended for tree traversals of the file system.
    /// It allows for fast paths where whole subtrees are skipped.
    /// It should be noted that the DirectoryMatch::ShouldTraverse return value is always correct.
    /// Other values enable fast code paths only (performance).
    fn matches_directory(&self, path: &RepoPath) -> Result<DirectoryMatch>;

    /// Returns true when the file path should be kept in the file set and returns false when
    /// it has to be removed.
    fn matches_file(&self, path: &RepoPath) -> Result<bool>;
}

/// Allows for fast code paths when dealing with patterns selecting directories.
/// `Everything` means that all the files in the subtree of the given directory need to be part
/// of the returned file set.
/// `Nothing` means that no files in the subtree of the given directory will be part of the
/// returned file set. Recursive traversal can be stopped at this point.
/// `ShouldTraverse` is a value that is always valid. It does not provide additional information.
/// Subtrees should be traversed and the matches should continue to be asked.
#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)]
pub enum DirectoryMatch {
    Everything,
    Nothing,
    ShouldTraverse,
}

impl<T: Matcher + ?Sized, U: Deref<Target = T>> Matcher for U {
    fn matches_directory(&self, path: &RepoPath) -> Result<DirectoryMatch> {
        T::matches_directory(self, path)
    }

    fn matches_file(&self, path: &RepoPath) -> Result<bool> {
        T::matches_file(self, path)
    }
}

#[derive(Clone, Debug)]
pub struct AlwaysMatcher {}

impl AlwaysMatcher {
    pub fn new() -> Self {
        AlwaysMatcher {}
    }
}

impl Matcher for AlwaysMatcher {
    fn matches_directory(&self, _path: &RepoPath) -> Result<DirectoryMatch> {
        Ok(DirectoryMatch::Everything)
    }
    fn matches_file(&self, _path: &RepoPath) -> Result<bool> {
        Ok(true)
    }
}

#[derive(Clone, Debug)]
pub struct NeverMatcher {}

impl NeverMatcher {
    pub fn new() -> Self {
        NeverMatcher {}
    }
}

impl Matcher for NeverMatcher {
    fn matches_directory(&self, _path: &RepoPath) -> Result<DirectoryMatch> {
        Ok(DirectoryMatch::Nothing)
    }
    fn matches_file(&self, _path: &RepoPath) -> Result<bool> {
        Ok(false)
    }
}

pub struct XorMatcher<A, B> {
    a: A,
    b: B,
}

impl<A, B> XorMatcher<A, B> {
    pub fn new(a: A, b: B) -> Self {
        XorMatcher { a, b }
    }
}

impl<A: Matcher, B: Matcher> Matcher for XorMatcher<A, B> {
    fn matches_directory(&self, path: &RepoPath) -> Result<DirectoryMatch> {
        let matches_a = self.a.matches_directory(path)?;
        let matches_b = self.b.matches_directory(path)?;
        Ok(match (matches_a, matches_b) {
            (DirectoryMatch::Everything, DirectoryMatch::Everything) => DirectoryMatch::Nothing,
            (DirectoryMatch::Nothing, DirectoryMatch::Nothing) => DirectoryMatch::Nothing,
            (DirectoryMatch::Everything, DirectoryMatch::Nothing) => DirectoryMatch::Everything,
            (DirectoryMatch::Nothing, DirectoryMatch::Everything) => DirectoryMatch::Everything,
            _ => DirectoryMatch::ShouldTraverse,
        })
    }

    fn matches_file(&self, path: &RepoPath) -> Result<bool> {
        Ok(self.a.matches_file(path)? ^ self.b.matches_file(path)?)
    }
}

pub struct DifferenceMatcher<A, B> {
    include: A,
    exclude: B,
}

impl<A, B> DifferenceMatcher<A, B> {
    pub fn new(include: A, exclude: B) -> Self {
        DifferenceMatcher { include, exclude }
    }
}

impl<A: Matcher, B: Matcher> Matcher for DifferenceMatcher<A, B> {
    fn matches_directory(&self, path: &RepoPath) -> Result<DirectoryMatch> {
        let include = self.include.matches_directory(path)?;

        // Don't execute the exclude ahead of time, since in some cases we can avoid executing it
        // entirely. This is useful when the exclude side is expensive, like in the status case
        // where the exclude side may inspect a manifest or the treestate.
        Ok(match include {
            DirectoryMatch::Nothing => DirectoryMatch::Nothing,
            DirectoryMatch::Everything => match self.exclude.matches_directory(path)? {
                DirectoryMatch::Nothing => DirectoryMatch::Everything,
                DirectoryMatch::Everything => DirectoryMatch::Nothing,
                DirectoryMatch::ShouldTraverse => DirectoryMatch::ShouldTraverse,
            },
            DirectoryMatch::ShouldTraverse => match self.exclude.matches_directory(path)? {
                DirectoryMatch::Everything => DirectoryMatch::Nothing,
                _ => DirectoryMatch::ShouldTraverse,
            },
        })
    }

    fn matches_file(&self, path: &RepoPath) -> Result<bool> {
        Ok(self.include.matches_file(path)? && !self.exclude.matches_file(path)?)
    }
}

pub struct UnionMatcher {
    matchers: Vec<Arc<dyn 'static + Matcher + Send + Sync>>,
}

impl UnionMatcher {
    pub fn new(matchers: Vec<Arc<dyn 'static + Matcher + Send + Sync>>) -> Self {
        UnionMatcher { matchers }
    }

    pub fn matches_directory<M: Matcher, I: Iterator<Item = M>>(
        matchers: I,
        path: &RepoPath,
    ) -> Result<DirectoryMatch> {
        let mut current = DirectoryMatch::Nothing;
        for matcher in matchers {
            current = match matcher.matches_directory(path)? {
                DirectoryMatch::Nothing => current,
                DirectoryMatch::Everything => {
                    return Ok(DirectoryMatch::Everything);
                }
                DirectoryMatch::ShouldTraverse => DirectoryMatch::ShouldTraverse,
            };
        }
        Ok(current)
    }

    pub fn matches_file<M: Matcher, I: Iterator<Item = M>>(
        matchers: I,
        path: &RepoPath,
    ) -> Result<bool> {
        for matcher in matchers {
            if matcher.matches_file(path)? {
                return Ok(true);
            }
        }
        Ok(false)
    }
}

impl Matcher for UnionMatcher {
    fn matches_directory(&self, path: &RepoPath) -> Result<DirectoryMatch> {
        UnionMatcher::matches_directory(self.matchers.iter(), path)
    }

    fn matches_file(&self, path: &RepoPath) -> Result<bool> {
        UnionMatcher::matches_file(self.matchers.iter(), path)
    }
}

pub struct IntersectMatcher {
    matchers: Vec<Arc<dyn 'static + Matcher + Send + Sync>>,
}

impl IntersectMatcher {
    pub fn new(matchers: Vec<Arc<dyn 'static + Matcher + Send + Sync>>) -> Self {
        Self { matchers }
    }
}

impl Matcher for IntersectMatcher {
    fn matches_directory(&self, path: &RepoPath) -> Result<DirectoryMatch> {
        if self.matchers.is_empty() {
            return Ok(DirectoryMatch::Nothing);
        }

        let mut traverse = false;
        for matcher in &self.matchers {
            match matcher.matches_directory(path)? {
                DirectoryMatch::Nothing => return Ok(DirectoryMatch::Nothing),
                DirectoryMatch::ShouldTraverse => traverse = true,
                DirectoryMatch::Everything => {}
            };
        }

        if traverse {
            Ok(DirectoryMatch::ShouldTraverse)
        } else {
            Ok(DirectoryMatch::Everything)
        }
    }

    fn matches_file(&self, path: &RepoPath) -> Result<bool> {
        let mut matched = false;
        for matcher in &self.matchers {
            if !matcher.matches_file(path)? {
                return Ok(false);
            }
            matched = true;
        }
        Ok(matched)
    }
}

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

    #[test]
    fn test_intersection_matcher() -> Result<()> {
        let empty = IntersectMatcher::new(Vec::new());
        assert_eq!(
            empty.matches_directory("something".try_into()?)?,
            DirectoryMatch::Nothing
        );
        assert!(!empty.matches_file("something".try_into()?)?);

        let matcher = IntersectMatcher::new(vec![
            Arc::new(ExactMatcher::new(
                [RepoPath::from_str("both/both")?, RepoPath::from_str("a/a")?].iter(),
                true,
            )),
            Arc::new(ExactMatcher::new(
                [RepoPath::from_str("both/both")?, RepoPath::from_str("b/b")?].iter(),
                true,
            )),
        ]);

        assert_eq!(
            matcher.matches_directory("both".try_into()?)?,
            DirectoryMatch::ShouldTraverse
        );
        assert_eq!(
            matcher.matches_directory("neither".try_into()?)?,
            DirectoryMatch::Nothing
        );
        assert_eq!(
            matcher.matches_directory("a".try_into()?)?,
            DirectoryMatch::Nothing
        );

        assert!(matcher.matches_file("both/both".try_into()?)?);
        assert!(!matcher.matches_file("neither".try_into()?)?);
        assert!(!matcher.matches_file("a/a".try_into()?)?);

        Ok(())
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use std::collections::HashMap;
use std::sync::Arc;

use anyhow::Result;

use crate::pattern::Pattern;
use crate::AlwaysMatcher;
use crate::DifferenceMatcher;
use crate::Error;
use crate::IntersectMatcher;
use crate::Matcher;
use crate::PatternKind;
use crate::RegexMatcher;
use crate::TreeMatcher;
use crate::UnionMatcher;

type DynMatcher = Arc<dyn 'static + Matcher + Send + Sync>;

/// Build matcher from normalized patterns.
///
/// The relationship between `patterns`, `include` and `exclude` is like:
///     (patterns & include) - exclude
///
/// `patterns`, `include`, `exclude` can be empty:
///   * If `patterns` is empty, we will build a AlwaysMatcher
///   * If `include` is empty, it will be ignored
///   * If `exclude` is empty it will be ignored
pub fn build_matcher(
    patterns: &[Pattern],
    include: &[Pattern],
    exclude: &[Pattern],
    case_sensitive: bool,
) -> Result<DynMatcher> {
    let mut m: DynMatcher = if patterns.is_empty() {
        Arc::new(AlwaysMatcher::new())
    } else {
        build_matcher_from_patterns(patterns, case_sensitive)?
    };

    if !include.is_empty() {
        let im = build_matcher_from_patterns(include, case_sensitive)?;
        m = Arc::new(IntersectMatcher::new(vec![m, im]));
    }

    if !exclude.is_empty() {
        let em = build_matcher_from_patterns(exclude, case_sensitive)?;
        m = Arc::new(DifferenceMatcher::new(m, em));
    }

    Ok(m)
}

fn build_matcher_from_patterns(patterns: &[Pattern], case_sensitive: bool) -> Result<DynMatcher> {
    assert!(!patterns.is_empty(), "patterns should not be empty");
    let grouped_patterns = group_by_pattern_kind(patterns);
    let mut matchers: Vec<DynMatcher> = Vec::new();
    for (kind, pats) in &grouped_patterns {
        let m: DynMatcher = match kind {
            PatternKind::Glob => Arc::new(TreeMatcher::from_rules(pats.iter(), case_sensitive)?),
            PatternKind::RE => {
                let regex_pat = format!("(?:{})", pats.join("|"));
                Arc::new(RegexMatcher::new(&regex_pat, case_sensitive)?)
            }
            _ => {
                return Err(Error::UnsupportedPatternKind(kind.name().to_string()).into());
            }
        };
        matchers.push(m);
    }

    if matchers.len() == 1 {
        Ok(matchers.remove(0))
    } else {
        Ok(Arc::new(UnionMatcher::new(matchers)))
    }
}

fn group_by_pattern_kind(patterns: &[Pattern]) -> HashMap<PatternKind, Vec<String>> {
    let mut res = HashMap::new();
    for p in patterns.iter() {
        res.entry(p.kind)
            .or_insert_with(Vec::new)
            .push(p.pattern.clone())
    }
    res
}

#[cfg(test)]
mod tests {
    use types::RepoPath;

    use super::*;
    use crate::DirectoryMatch;

    macro_rules! path {
        ($s:expr) => {
            RepoPath::from_str($s).unwrap()
        };
    }

    #[test]
    fn test_build_matcher_with_all_empty() {
        // AlwaysMatcher
        let m = build_matcher(&[], &[], &[], true).unwrap();

        assert!(m.matches_file(path!("")).unwrap());
        assert!(m.matches_file(path!("a")).unwrap());
        assert!(m.matches_file(path!("a/b")).unwrap());
        assert!(m.matches_file(path!("z")).unwrap());
    }

    #[test]
    fn test_build_matcher_with_all_non_empty() {
        let patterns = &[Pattern::new(PatternKind::RE, r"a/t\d+.*\.py".to_string())];
        let include = &[Pattern::new(PatternKind::Glob, "a/t1*/**".to_string())];
        let exclude = &[Pattern::new(PatternKind::Glob, "a/t11/**".to_string())];

        let m = build_matcher(patterns, include, exclude, true).unwrap();

        assert_eq!(
            m.matches_directory(path!("a")).unwrap(),
            DirectoryMatch::ShouldTraverse
        );
        assert_eq!(
            m.matches_directory(path!("a/t1")).unwrap(),
            DirectoryMatch::ShouldTraverse
        );
        assert_eq!(
            m.matches_directory(path!("a/t11")).unwrap(),
            DirectoryMatch::Nothing
        );
        assert_eq!(
            m.matches_directory(path!("a/tt")).unwrap(),
            DirectoryMatch::Nothing
        );
        assert_eq!(
            m.matches_directory(path!("b")).unwrap(),
            DirectoryMatch::Nothing
        );

        assert!(m.matches_file(path!("a/t1/b.py")).unwrap());
        assert!(m.matches_file(path!("a/t12/b.py")).unwrap());
        assert!(!m.matches_file(path!("a/t11/b.py")).unwrap());
        assert!(!m.matches_file(path!("b/b.py")).unwrap());
    }

    #[test]
    fn test_build_matcher_with_empty_patterns() {
        let include = &[Pattern::new(PatternKind::Glob, "a/t1*/**".to_string())];
        let exclude = &[Pattern::new(PatternKind::Glob, "a/t11/**".to_string())];

        let m = build_matcher(&[], include, exclude, true).unwrap();

        assert_eq!(
            m.matches_directory(path!("a/t1a")).unwrap(),
            DirectoryMatch::Everything
        );
        assert_eq!(
            m.matches_directory(path!("a/t11")).unwrap(),
            DirectoryMatch::Nothing
        );
    }

    #[test]
    fn test_build_matcher_with_empty_include() {
        let patterns = &[Pattern::new(PatternKind::RE, r"a/t\d+.*\.py".to_string())];
        let exclude = &[Pattern::new(PatternKind::Glob, "a/t11/**".to_string())];

        let m = build_matcher(patterns, &[], exclude, true).unwrap();

        assert_eq!(
            m.matches_directory(path!("a/t1")).unwrap(),
            DirectoryMatch::ShouldTraverse
        );
        assert_eq!(
            m.matches_directory(path!("a/t11")).unwrap(),
            DirectoryMatch::Nothing
        );
    }

    #[test]
    fn test_build_matcher_with_empty_exclude() {
        let patterns = &[Pattern::new(PatternKind::RE, r"a/t\d+.*\.py".to_string())];
        let include = &[Pattern::new(PatternKind::Glob, "a/t1*/**".to_string())];

        let m = build_matcher(patterns, include, &[], true).unwrap();

        assert_eq!(
            m.matches_directory(path!("a/t1")).unwrap(),
            DirectoryMatch::ShouldTraverse
        );
        assert_eq!(
            m.matches_directory(path!("a/t11")).unwrap(),
            DirectoryMatch::ShouldTraverse
        );
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use std::str::FromStr;

use crate::error::Error;

#[derive(Debug, PartialEq, Copy, Clone, Hash, Eq)]
pub enum PatternKind {
    /// a regular expression relative to repository root, check [RegexMatcher]
    /// for supported RE syntax
    RE,

    /// a shell-style glob pattern relative to cwd
    Glob,

    /// a path relative to the repository root, and when the path points to a
    /// directory, it is matched recursively
    Path,

    /// an unrooted glob (e.g.: *.c matches C files in all dirs)
    RelGlob,

    /// a path relative to cwd
    RelPath,

    /// an unrooted regular expression, needn't match the start of a path
    RelRE,

    /// read file patterns per line from a file
    ListFile,

    /// read file patterns with null byte delimiters from a file
    ListFile0,

    /// a fileset expression
    Set,

    /// a file of patterns to read and include
    Include,

    /// a file of patterns to match against files under the same directory
    SubInclude,

    /// a path relative to repository root, which is matched non-recursively (will
    /// not match subdirectories)
    RootFilesIn,
}

impl PatternKind {
    pub fn name(&self) -> &'static str {
        match self {
            PatternKind::RE => "re",
            PatternKind::Glob => "glob",
            PatternKind::Path => "path",
            PatternKind::RelGlob => "relglob",
            PatternKind::RelPath => "relpath",
            PatternKind::RelRE => "relre",
            PatternKind::ListFile => "listfile",
            PatternKind::ListFile0 => "listfile0",
            PatternKind::Set => "set",
            PatternKind::Include => "include",
            PatternKind::SubInclude => "subinclude",
            PatternKind::RootFilesIn => "rootfilesin",
        }
    }
}

impl std::str::FromStr for PatternKind {
    type Err = Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "re" => Ok(PatternKind::RE),
            "glob" => Ok(PatternKind::Glob),
            "path" => Ok(PatternKind::Path),
            "relglob" => Ok(PatternKind::RelGlob),
            "relpath" => Ok(PatternKind::RelPath),
            "relre" => Ok(PatternKind::RelRE),
            "listfile" => Ok(PatternKind::ListFile),
            "listfile0" => Ok(PatternKind::ListFile0),
            "set" => Ok(PatternKind::Set),
            "include" => Ok(PatternKind::Include),
            "subinclude" => Ok(PatternKind::SubInclude),
            "rootfilesin" => Ok(PatternKind::RootFilesIn),
            _ => Err(Error::UnsupportedPatternKind(s.to_string())),
        }
    }
}

#[derive(Debug, PartialEq)]
pub struct Pattern {
    pub(crate) kind: PatternKind,
    pub(crate) pattern: String,
    pub(crate) source: Option<String>,
}

impl Pattern {
    pub(crate) fn new(kind: PatternKind, pattern: String) -> Self {
        Self {
            kind,
            pattern,
            source: None,
        }
    }

    pub(crate) fn with_source(mut self, source: String) -> Self {
        self.source = Some(source);
        self
    }

    /// Build `Pattern` from str.
    ///
    /// * If the str doesn't have pattern kind prefix, we will use `default_kind`.
    /// * `source` is set to None.
    pub(crate) fn from_str(pattern: &str, default_kind: PatternKind) -> Self {
        let (kind, pat) = split_pattern(pattern, default_kind);
        Self {
            kind,
            pattern: pat.to_string(),
            source: None,
        }
    }
}

/// Build `Pattern`s from strings. It calls `Pattern::from_str` to do actual work.
pub fn build_patterns(patterns: &[String], default_kind: PatternKind) -> Vec<Pattern> {
    patterns
        .iter()
        .map(|s| Pattern::from_str(s, default_kind))
        .collect()
}

pub fn split_pattern<'a>(pattern: &'a str, default_kind: PatternKind) -> (PatternKind, &'a str) {
    match pattern.split_once(':') {
        Some((k, p)) => {
            if let Ok(kind) = PatternKind::from_str(k) {
                (kind, p)
            } else {
                (default_kind, pattern)
            }
        }
        None => (default_kind, pattern),
    }
}

// TODO: refactor this code to avoid the overhead of monomorphization by
// using a wrapper function.
#[allow(dead_code)]
pub(crate) fn normalize_patterns<I>(
    patterns: I,
    default_kind: PatternKind,
) -> Result<Vec<Pattern>, Error>
where
    I: IntoIterator,
    I::Item: AsRef<str>,
{
    let mut result: Vec<Pattern> = Vec::new();
    for pattern in patterns {
        let pattern = pattern.as_ref();
        let (kind, pat) = split_pattern(pattern, default_kind);
        match kind {
            PatternKind::RelPath | PatternKind::Glob => {
                // TODO: need to implement pathutil.pathauditor and pathutil.canonpath
                // https://fburl.com/code/0q9sgvbj
                result.push(Pattern::new(kind, pat.to_string()));
            }
            PatternKind::RelGlob | PatternKind::Path | PatternKind::RootFilesIn => {
                let normalized_pat = normalize_path_pattern(pat);
                result.push(Pattern::new(kind, normalized_pat));
            }
            PatternKind::ListFile | PatternKind::ListFile0 => {
                let contents = util::file::read_to_string(pat)?;
                let sep = if kind == PatternKind::ListFile {
                    '\n'
                } else {
                    '\0'
                };
                let lines = contents.split(sep);
                for p in normalize_patterns(lines, default_kind)? {
                    let p = p.with_source(pat.to_string());
                    result.push(p);
                }
            }
            PatternKind::Set | PatternKind::Include | PatternKind::SubInclude => {
                return Err(Error::UnsupportedPatternKind(kind.name().to_string()));
            }
            _ => result.push(Pattern::new(kind, pat.to_string())),
        }
    }
    Ok(result)
}

/// A wrapper of `util::path::normalize` function by adding path separator convertion,
/// yields normalized [String] if the pattern is valid unicode.
///
/// This function normalize the path difference on Windows by converting
/// path separator from `\` to `/`. This is need because our `RepoPathBuf`
/// is a path separated by `/`.
fn normalize_path_pattern(pattern: &str) -> String {
    let pattern = util::path::normalize(pattern.as_ref());
    // SAFTEY: In Rust, values of type String are always valid UTF-8.
    // Our input pattern is a &str, and we don't add invalid chars in
    // out `util::path::normalize` function, so it should be safe here.
    let pattern_str = pattern.to_string_lossy();
    if cfg!(windows) {
        pattern_str.replace(
            std::path::MAIN_SEPARATOR,
            &types::path::SEPARATOR.to_string(),
        )
    } else {
        pattern_str.to_string()
    }
}

#[cfg(test)]
mod tests {

    use std::fs;

    use tempfile::TempDir;

    use super::*;

    #[test]
    fn test_split_pattern() {
        let v = split_pattern("re:a.*py", PatternKind::Glob);
        assert_eq!(v, (PatternKind::RE, "a.*py"));

        let v = split_pattern("badkind:a.*py", PatternKind::Glob);
        assert_eq!(v, (PatternKind::Glob, "badkind:a.*py"));

        let v = split_pattern("a.*py", PatternKind::RE);
        assert_eq!(v, (PatternKind::RE, "a.*py"));
    }

    #[test]
    fn test_pattern_kind_enum() {
        assert_eq!(PatternKind::from_str("re").unwrap(), PatternKind::RE);
        assert!(PatternKind::from_str("invalid").is_err());

        assert_eq!(PatternKind::RE.name(), "re");
    }

    #[test]
    fn test_normalize_path_pattern() {
        assert_eq!(
            normalize_path_pattern("foo/bar/../baz/"),
            "foo/baz".to_string()
        );
    }

    #[test]
    fn test_normalize_patterns() {
        assert_eq!(
            normalize_patterns(
                vec!["glob:/a/*", r"re:a.*\.py", "path:foo/bar/../baz/"],
                PatternKind::Glob
            )
            .unwrap(),
            [
                Pattern::new(PatternKind::Glob, "/a/*".to_string()),
                Pattern::new(PatternKind::RE, r"a.*\.py".to_string()),
                Pattern::new(PatternKind::Path, "foo/baz".to_string()),
            ]
        );
        assert_eq!(
            normalize_patterns(vec!["/a/*", r"re:a.*\.py"], PatternKind::Glob).unwrap(),
            [
                Pattern::new(PatternKind::Glob, "/a/*".to_string()),
                Pattern::new(PatternKind::RE, r"a.*\.py".to_string()),
            ]
        );
        assert_eq!(
            normalize_patterns(vec!["relglob:*.c"], PatternKind::Glob).unwrap(),
            [Pattern::new(PatternKind::RelGlob, "*.c".to_string()),]
        );
    }

    #[test]
    fn test_normalize_patterns_unsupported_kind() {
        assert!(normalize_patterns(vec!["set:added()"], PatternKind::Glob).is_err());
        assert!(normalize_patterns(vec!["include:/a/b.txt"], PatternKind::Glob).is_err());
        assert!(normalize_patterns(vec!["subinclude:/a/b.txt"], PatternKind::Glob).is_err());
    }

    #[test]
    fn test_build_patterns() {
        let patterns = ["re:a.py".to_string(), "a.txt".to_string()];

        assert_eq!(
            build_patterns(&patterns, PatternKind::Glob),
            [
                Pattern::new(PatternKind::RE, "a.py".to_string()),
                Pattern::new(PatternKind::Glob, "a.txt".to_string())
            ]
        )
    }

    #[test]
    fn test_normalize_patterns_listfile() {
        test_normalize_patterns_listfile_helper("\n");
    }

    #[test]
    fn test_normalize_patterns_listfile0() {
        test_normalize_patterns_listfile_helper("\0");
    }

    fn test_normalize_patterns_listfile_helper(sep: &str) {
        let inner_patterns = vec!["glob:/a/*", r"re:a.*\.py"];
        let dir = TempDir::new().unwrap();
        let path = dir.path().join("patterns.txt");
        let path_str = path.to_string_lossy();
        let content = inner_patterns.join(sep);
        fs::write(&path, content).unwrap();

        let outer_patterns = vec![format!(
            "listfile{}:{}",
            if sep == "\n" { "" } else { "0" },
            path_str
        )];
        let result = normalize_patterns(outer_patterns, PatternKind::Glob).unwrap();

        assert_eq!(
            result,
            [
                Pattern::new(PatternKind::Glob, "/a/*".to_string())
                    .with_source(path_str.to_string()),
                Pattern::new(PatternKind::RE, r"a.*\.py".to_string())
                    .with_source(path_str.to_string())
            ]
        )
    }
}