/*
 * 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::hash_map::DefaultHasher;
use std::collections::HashMap;
use std::hash::Hash;
use std::hash::Hasher;
use std::ops::DerefMut;
use std::path::Path;
use std::sync::Arc;

use anyhow::anyhow;
use anyhow::Error;
use async_runtime::try_block_unless_interrupted;
use configmodel::Config;
use configmodel::ConfigExt;
use manifest::FileMetadata;
use manifest::FsNodeMetadata;
use manifest::Manifest;
use parking_lot::Mutex;
use pathmatcher::ExactMatcher;
use pathmatcher::Matcher;
use pathmatcher::UnionMatcher;
pub use sparse::Root;
use storemodel::futures::StreamExt;
use storemodel::ReadFileContents;
use types::Key;
use types::RepoPath;
use types::RepoPathBuf;
use vfs::VFS;

pub static CONFIG_OVERRIDE_CACHE: &str = "sparseprofileconfigs";
pub static MERGE_FILE_OVERRIDES: &str = "tempsparse";

pub fn repo_matcher(
    vfs: &VFS,
    dot_path: &Path,
    manifest: impl Manifest + Send + Sync + 'static,
    store: Arc<dyn ReadFileContents<Error = anyhow::Error> + Send + Sync>,
) -> anyhow::Result<Option<(Arc<dyn Matcher + Send + Sync + 'static>, u64)>> {
    repo_matcher_with_overrides(vfs, dot_path, manifest, store, &disk_overrides(dot_path)?)
}

pub fn repo_matcher_with_overrides(
    vfs: &VFS,
    dot_path: &Path,
    manifest: impl Manifest + Send + Sync + 'static,
    store: Arc<dyn ReadFileContents<Error = anyhow::Error> + Send + Sync>,
    overrides: &HashMap<String, String>,
) -> anyhow::Result<Option<(Arc<dyn Matcher + Send + Sync + 'static>, u64)>> {
    let prof = match util::file::read(dot_path.join("sparse")) {
        Ok(contents) => sparse::Root::from_bytes(contents, ".hg/sparse".to_string())?,
        Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
            return Ok(None);
        }
        Err(e) => {
            return Err(e.into());
        }
    };

    Ok(Some(build_matcher(
        vfs,
        dot_path,
        &prof,
        manifest,
        store.clone(),
        overrides,
    )?))
}

fn build_matcher(
    vfs: &VFS,
    dot_path: &Path,
    prof: &sparse::Root,
    manifest: impl Manifest + Send + Sync + 'static,
    store: Arc<dyn ReadFileContents<Error = anyhow::Error> + Send + Sync>,
    overrides: &HashMap<String, String>,
) -> anyhow::Result<(Arc<dyn Matcher + Send + Sync + 'static>, u64)> {
    let manifest = Arc::new(manifest);

    let hasher = Mutex::new(DefaultHasher::new());
    prof.hash(hasher.lock().deref_mut());

    let matcher = try_block_unless_interrupted(prof.matcher(|path| async {
        let path = path;

        let file_id = {
            let manifest = manifest.clone();
            let repo_path = RepoPathBuf::from_string(path.clone())?;

            // Work around nested block_on() calls by spawning a new thread.
            // Once the Manifest is async this can go away.
            tokio::task::spawn_blocking(move || match manifest.get(&repo_path)? {
                None => {
                    tracing::warn!(?repo_path, "non-existent sparse profile include");
                    Ok::<_, Error>(None)
                }
                Some(fs_node) => match fs_node {
                    FsNodeMetadata::File(FileMetadata { hgid, .. }) => Ok(Some(hgid)),
                    FsNodeMetadata::Directory(_) => {
                        tracing::warn!(?repo_path, "sparse profile include is a directory");
                        Ok(None)
                    }
                },
            })
            .await??
        };

        let file_id = match file_id {
            Some(id) => id,
            None => return Ok(None),
        };

        let repo_path = RepoPathBuf::from_string(path.clone())?;
        let mut stream = store
            .read_file_contents(vec![Key::new(repo_path.clone(), file_id.clone())])
            .await;
        match stream.next().await {
            Some(Ok((bytes, _key))) => {
                let mut bytes = bytes.into_vec();
                if let Some(extra) = overrides.get(&path) {
                    bytes.append(&mut extra.to_string().into_bytes());
                }
                bytes.hash(hasher.lock().deref_mut());
                Ok(Some(bytes))
            }
            Some(Err(err)) => Err(err),
            None => Err(anyhow!("no contents for {}", repo_path)),
        }
    }))?;

    let mut matcher: Arc<dyn Matcher + Send + Sync + 'static> = Arc::new(matcher);

    match util::file::read_to_string(dot_path.join(MERGE_FILE_OVERRIDES)) {
        Ok(temp) => {
            temp.hash(hasher.lock().deref_mut());
            let exact = ExactMatcher::new(
                temp.split('\n')
                    .map(|p| p.try_into())
                    .collect::<Result<Vec<&RepoPath>, _>>()?
                    .iter(),
                vfs.case_sensitive(),
            );
            matcher = Arc::new(UnionMatcher::new(vec![Arc::new(exact), matcher]));
        }
        Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
        Err(err) => return Err(err.into()),
    }

    let hash = hasher.lock().finish();
    Ok((matcher, hash))
}

pub fn config_overrides(config: impl Config) -> HashMap<String, String> {
    let mut overrides: HashMap<String, String> = HashMap::new();
    for key in config.keys("sparseprofile") {
        let parts: Vec<&str> = key.splitn(3, '.').collect();
        if parts.len() != 3 {
            tracing::warn!(?key, "invalid sparseprofile config key");
            continue;
        }

        let (sparse_section, prof_name) = (parts[0], parts[2]);

        let vals = match config.get_or_default::<Vec<String>>("sparseprofile", &key) {
            Ok(vals) => vals,
            Err(err) => {
                tracing::warn!(?key, ?err, "invalid sparseprofile config value");
                continue;
            }
        };

        overrides
            .entry(prof_name.into())
            .or_default()
            .push_str(&format!(
                "\n# source = hgrc.dynamic \"{}\"\n[{}]\n{}\n# source =\n",
                key,
                sparse_section,
                vals.join("\n")
            ));
    }

    overrides
}

fn disk_overrides(dot_path: &Path) -> anyhow::Result<HashMap<String, String>> {
    match util::file::open(dot_path.join(CONFIG_OVERRIDE_CACHE), "r") {
        Ok(f) => Ok(serde_json::from_reader(f)?),
        Err(err) if err.kind() != std::io::ErrorKind::NotFound => Err(err.into()),
        _ => Ok(HashMap::new()),
    }
}

#[cfg(test)]
mod tests {
    use std::collections::BTreeMap;

    use futures::stream;
    use futures::stream::BoxStream;
    use types::HgId;
    use types::Parents;
    use types::RepoPath;

    use super::*;

    #[test]
    fn test_config_overrides() -> anyhow::Result<()> {
        let mut conf = BTreeMap::new();

        conf.insert("sparseprofile.include.foo.someprof", "inca,incb");
        conf.insert("sparseprofile.include.bar.someprof", "incc");
        conf.insert("sparseprofile.exclude.foo.someprof", "exca,excb");

        conf.insert("sparseprofile.include.foo.otherprof", "inca");

        let overrides = config_overrides(&conf);
        assert_eq!(
            overrides,
            HashMap::from([
                (
                    "someprof".to_string(),
                    r#"
# source = hgrc.dynamic "exclude.foo.someprof"
[exclude]
exca
excb
# source =

# source = hgrc.dynamic "include.bar.someprof"
[include]
incc
# source =

# source = hgrc.dynamic "include.foo.someprof"
[include]
inca
incb
# source =
"#
                    .to_string()
                ),
                (
                    "otherprof".to_string(),
                    r#"
# source = hgrc.dynamic "include.foo.otherprof"
[include]
inca
# source =
"#
                    .to_string()
                ),
            ])
        );

        let dir = tempfile::tempdir()?;

        {
            let f = util::file::create(dir.path().join(CONFIG_OVERRIDE_CACHE))?;
            serde_json::to_writer(f, &overrides)?;
        }

        let roundtrip_overrides = disk_overrides(dir.path())?;
        assert_eq!(roundtrip_overrides, overrides);

        Ok(())
    }

    #[test]
    fn test_build_matcher() -> anyhow::Result<()> {
        let root_dir = tempfile::tempdir()?;
        let vfs = VFS::new(root_dir.path().to_path_buf())?;

        let mut config = BTreeMap::new();

        config.insert("sparseprofile.exclude.blah.tools/sparse/base", "inc/exc");

        let mut commit = StubCommit::new();
        commit.insert(
            "tools/sparse/base",
            "[include]
inc

# ignore this non-existent profile
%include bogus-ignore-me

# ignore include of directory
%include tools/sparse

[exclude]
exc",
        );

        let (matcher, _hash) = build_matcher(
            &vfs,
            root_dir.path(),
            &sparse::Root::from_bytes(b"%include tools/sparse/base", "root".to_string())?,
            commit.clone(),
            Arc::new(commit.clone()),
            &config_overrides(&config),
        )?;

        assert!(matcher.matches_file("inc/banana".try_into()?)?);
        assert!(!matcher.matches_file("exc/banana".try_into()?)?);
        assert!(!matcher.matches_file("merge/a".try_into()?)?);

        // Test the config override.
        assert!(!matcher.matches_file("inc/exc/foo".try_into()?)?);

        std::fs::write(
            root_dir.path().join(MERGE_FILE_OVERRIDES),
            "merge/a\nmerge/b",
        )?;

        let (matcher, _hash) = build_matcher(
            &vfs,
            root_dir.path(),
            &sparse::Root::from_bytes(b"%include tools/sparse/base", "root".to_string())?,
            commit.clone(),
            Arc::new(commit.clone()),
            &config_overrides(&config),
        )?;

        assert!(matcher.matches_file("merge/a".try_into()?)?);
        assert!(matcher.matches_file("merge/b".try_into()?)?);
        assert!(!matcher.matches_file("merge/abc".try_into()?)?);

        Ok(())
    }

    #[test]
    fn test_matcher_hashes() -> anyhow::Result<()> {
        let root_dir = tempfile::tempdir()?;
        let vfs = VFS::new(root_dir.path().to_path_buf())?;

        let config: BTreeMap<String, String> = BTreeMap::new();

        let mut commit = StubCommit::new();
        commit.insert(
            "tools/sparse/base",
            "[include]
inc

[exclude]
exc",
        );

        let (_matcher, hash) = build_matcher(
            &vfs,
            root_dir.path(),
            &sparse::Root::from_bytes(b"%include tools/sparse/base", "root".to_string())?,
            commit.clone(),
            Arc::new(commit.clone()),
            &config_overrides(&config),
        )?;

        let mut commit = StubCommit::new();
        commit.insert(
            "tools/sparse/base",
            "[include]
inc

[exclude]
exc",
        );

        let (_matcher, same_hash) = build_matcher(
            &vfs,
            root_dir.path(),
            &sparse::Root::from_bytes(b"%include tools/sparse/base", "root".to_string())?,
            commit.clone(),
            Arc::new(commit.clone()),
            &config_overrides(&config),
        )?;

        assert!(hash == same_hash, "hashes should match if contents matches");

        let (_matcher, different_hash_config_change) = build_matcher(
            &vfs,
            root_dir.path(),
            &sparse::Root::from_bytes(
                b"%include tools/sparse/base
[include]
config_inc
",
                "root".to_string(),
            )?,
            commit.clone(),
            Arc::new(commit.clone()),
            &config_overrides(&config),
        )?;

        assert_ne!(
            hash, different_hash_config_change,
            "hashes should not match if contents do not match"
        );

        let mut commit = StubCommit::new();
        commit.insert(
            "tools/sparse/base",
            "[include]
inc
",
        );

        let (_matcher, different_hash_profile_change) = build_matcher(
            &vfs,
            root_dir.path(),
            &sparse::Root::from_bytes(b"%include tools/sparse/base", "root".to_string())?,
            commit.clone(),
            Arc::new(commit.clone()),
            &config_overrides(&config),
        )?;

        assert_ne!(
            hash, different_hash_profile_change,
            "hashes should not match if contents do not match"
        );

        Ok(())
    }

    #[derive(Clone)]
    struct StubCommit {
        files: HashMap<RepoPathBuf, Vec<u8>>,
    }

    impl StubCommit {
        fn new() -> Self {
            StubCommit {
                files: HashMap::new(),
            }
        }

        fn insert(&mut self, path: impl AsRef<str>, contents: impl AsRef<[u8]>) {
            self.files.insert(
                path.as_ref().to_string().try_into().unwrap(),
                contents.as_ref().to_vec(),
            );
        }

        fn file_id(&self, path: &RepoPath) -> Option<HgId> {
            self.files
                .get(path)
                .map(|data| HgId::from_content(data, Parents::None))
        }
    }

    #[allow(unused_variables)]
    impl Manifest for StubCommit {
        fn get(&self, path: &RepoPath) -> anyhow::Result<Option<FsNodeMetadata>> {
            match self.file_id(path) {
                None => Ok(None),
                Some(id) => Ok(Some(FsNodeMetadata::File(FileMetadata::new(
                    id,
                    manifest::FileType::Regular,
                )))),
            }
        }

        fn get_ignore_case(&self, path: &RepoPath) -> anyhow::Result<Option<FsNodeMetadata>> {
            unimplemented!("get_ignore_case not implemented for StubCommit")
        }

        fn list(&self, path: &RepoPath) -> anyhow::Result<manifest::List> {
            unimplemented!()
        }

        fn insert(
            &mut self,
            file_path: RepoPathBuf,
            file_metadata: FileMetadata,
        ) -> anyhow::Result<()> {
            unimplemented!()
        }

        fn remove(&mut self, file_path: &RepoPath) -> anyhow::Result<Option<FileMetadata>> {
            unimplemented!()
        }

        fn flush(&mut self) -> anyhow::Result<HgId> {
            unimplemented!()
        }

        fn files<'a, M: 'static + pathmatcher::Matcher + Sync + Send>(
            &'a self,
            matcher: M,
        ) -> Box<dyn Iterator<Item = anyhow::Result<manifest::File>> + 'a> {
            unimplemented!()
        }

        fn dirs<'a, M: 'static + pathmatcher::Matcher + Sync + Send>(
            &'a self,
            matcher: M,
        ) -> Box<dyn Iterator<Item = anyhow::Result<manifest::Directory>> + 'a> {
            unimplemented!()
        }

        fn diff<'a, M: pathmatcher::Matcher>(
            &'a self,
            other: &'a Self,
            matcher: &'a M,
        ) -> anyhow::Result<Box<dyn Iterator<Item = anyhow::Result<manifest::DiffEntry>> + 'a>>
        {
            unimplemented!()
        }

        fn modified_dirs<'a, M: pathmatcher::Matcher>(
            &'a self,
            other: &'a Self,
            matcher: &'a M,
        ) -> anyhow::Result<Box<dyn Iterator<Item = anyhow::Result<manifest::DirDiffEntry>> + 'a>>
        {
            unimplemented!()
        }
    }

    #[async_trait::async_trait]
    impl ReadFileContents for StubCommit {
        type Error = anyhow::Error;

        async fn read_file_contents(
            &self,
            keys: Vec<Key>,
        ) -> BoxStream<Result<(storemodel::minibytes::Bytes, Key), Self::Error>> {
            stream::iter(keys.into_iter().map(|k| match self.file_id(&k.path) {
                None => Err(anyhow!("no such path")),
                Some(id) if id == k.hgid => {
                    Ok((self.files.get(&k.path).unwrap().clone().into(), k))
                }
                Some(_) => Err(anyhow!("bad file id")),
            }))
            .boxed()
        }

        async fn read_rename_metadata(
            &self,
            _keys: Vec<Key>,
        ) -> BoxStream<Result<(Key, Option<Key>), Self::Error>> {
            stream::empty().boxed()
        }
    }
}


/*
 * 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 manifest::Manifest;
use manifest_tree::ReadTreeManifest;
use manifest_tree::TreeManifest;
use parking_lot::Mutex;
use parking_lot::RwLock;
use pathmatcher::DifferenceMatcher;
use pathmatcher::ExactMatcher;
use pathmatcher::Matcher;
use status::StatusBuilder;
use tracing::trace;
use treestate::filestate::StateFlags;
use treestate::treestate::TreeState;
use types::HgId;
use types::RepoPathBuf;

use crate::filesystem::ChangeType;
use crate::util::walk_treestate;
use crate::walker::WalkError;

struct FakeTreeResolver {
    pub manifest: Arc<RwLock<TreeManifest>>,
}

impl ReadTreeManifest for FakeTreeResolver {
    fn get(&self, _commit_id: &HgId) -> Result<Arc<RwLock<TreeManifest>>> {
        Ok(self.manifest.clone())
    }
}

/// Compute the status of the working copy relative to the current commit.
#[allow(unused_variables)]
#[tracing::instrument(skip_all)]
pub fn compute_status(
    p1_manifest: &impl Manifest,
    treestate: Arc<Mutex<TreeState>>,
    pending_changes: impl Iterator<Item = Result<ChangeType>>,
    matcher: Arc<dyn Matcher + Send + Sync + 'static>,
) -> Result<StatusBuilder> {
    let mut modified = vec![];
    let mut added = vec![];
    let mut removed = vec![];
    let mut deleted = vec![];
    let mut unknown = vec![];
    let mut invalid_path = vec![];
    let mut invalid_type = vec![];

    // Step 1: get the tree state for each pending change in the working copy.
    // We may have a TreeState that only holds files that are being added/removed
    // (for example, in a repo backed by EdenFS). In this case, we need to make a note
    // of these paths to later query the manifest to determine if they're known or unknown files.

    // Changed files that don't exist in the TreeState. Maps to (is_deleted, in_manifest).
    let mut manifest_files = HashMap::<RepoPathBuf, (bool, bool)>::new();
    for change in pending_changes {
        let (path, is_deleted) = match change {
            Ok(ChangeType::Changed(path)) => (path, false),
            Ok(ChangeType::Deleted(path)) => (path, true),
            Err(e) => {
                let e = match e.downcast::<types::path::ParseError>() {
                    Ok(parse_err) => {
                        invalid_path.push(parse_err.into_path_bytes());
                        continue;
                    }
                    Err(e) => e,
                };

                let e = match e.downcast::<WalkError>() {
                    Ok(walk_err) => {
                        match walk_err {
                            WalkError::InvalidFileType(path) => {
                                invalid_type.push(path);
                                continue;
                            }
                            WalkError::RepoPathError(_, err) => {
                                invalid_path.push(err.into_path_bytes());
                                continue;
                            }
                            WalkError::FsUtf8Error(path) => {
                                invalid_path.push(path.into_bytes());
                                continue;
                            }
                            _ => {}
                        }

                        walk_err.into()
                    }
                    Err(e) => e,
                };

                return Err(e);
            }
        };

        let mut treestate = treestate.lock();
        match treestate.normalized_get(&path)? {
            Some(state) => {
                let exist_parent = state
                    .state
                    .intersects(StateFlags::EXIST_P1 | StateFlags::EXIST_P2);
                let exist_next = state.state.contains(StateFlags::EXIST_NEXT);
                let copied = state.state.contains(StateFlags::COPIED);

                trace!(%path, is_deleted, exist_parent, exist_next, copied);

                match (is_deleted, exist_parent, exist_next, copied) {
                    (_, true, false, _) => removed.push(path),
                    (true, true, true, _) => deleted.push(path),
                    (false, true, true, _) => modified.push(path),
                    (false, false, true, _) => added.push(path),
                    // This happens on EdenFS when a modified file is
                    // renamed over another existing file.
                    (false, false, false, true) => modified.push(path),
                    (false, false, false, false) => unknown.push(path),
                    _ => {
                        // The remaining case is (T, F, _, _).
                        // If the file is deleted, but didn't exist in a parent commit,
                        // it didn't change.
                    }
                }
            }
            None => {
                trace!(%path, is_deleted, "not in dirstate");

                // Path not found in the TreeState, so we need to query the manifest
                // to determine if this is a known or unknown file.
                manifest_files.insert(path, (is_deleted, false));
            }
        }
    }
    // Handle changed files we didn't find in the TreeState.
    p1_manifest
        .files(ExactMatcher::new(
            manifest_files.keys(),
            true, // case_sensitive = true
        ))
        .filter_map(Result::ok)
        .for_each(|file| {
            if let Some(entry) = manifest_files.get_mut(&file.path) {
                entry.1 = true;
            }
        });
    for (path, (is_deleted, in_manifest)) in manifest_files {
        // `exist_parent = in_manifest`. Also, `exist_parent = in_manifest`:
        // If a file existed in the manifest but didn't EXIST_NEXT,
        // it would be a "removed" file (and thus would definitely be in the TreeState).
        // Similarly, if a file doesn't exist in the manifest but did EXIST_NEXT,
        // it would be an "added" file.
        // This is a subset of the logic above.
        trace!(%path, is_deleted, in_manifest, "manifest file");

        match (is_deleted, in_manifest) {
            (true, true) => deleted.push(path),
            (false, true) => modified.push(path),
            (false, false) => unknown.push(path),
            (true, false) => {} // Deleted, but didn't exist in a parent commit.
        }
    }

    // Step 2: handle files that aren't in pending changes.
    // We can't directly check the filesystem at this layer. Instead, we need to infer:
    // a file that isn't in P1 and isn't in "pending changes" doesn't exist on the filesystem.
    let seen = std::iter::empty()
        .chain(modified.iter())
        .chain(added.iter())
        .chain(removed.iter())
        .chain(deleted.iter())
        .chain(unknown.iter());

    // Augment matcher to skip "seen" files since they have already been handled above.
    let matcher = Arc::new(DifferenceMatcher::new(
        matcher,
        ExactMatcher::new(seen, true),
    ));

    let mut treestate = treestate.lock();

    // A file that's "added" in the tree (doesn't exist in a parent, but exists in the next
    // commit) but isn't in "pending changes" must have been deleted on the filesystem.
    walk_treestate(
        &mut treestate,
        matcher.clone(),
        StateFlags::EXIST_NEXT,
        StateFlags::EXIST_P1 | StateFlags::EXIST_P2,
        |path, state| {
            trace!(%path, "deleted (added file not in pending changes)");
            deleted.push(path);
            Ok(())
        },
    )?;

    // Pending changes shows changes in the working copy with respect to P1.
    // Thus, we need to specially handle files that are in P2.
    walk_treestate(
        &mut treestate,
        matcher.clone(),
        StateFlags::EXIST_P2,
        StateFlags::empty(),
        |path, state| {
            // If it's in P1 but we didn't see it earlier, that means it didn't change with
            // respect to P1. But since it is marked EXIST_P2, that means P2 changed it and
            // therefore we should report it as changed.
            if state.state.contains(StateFlags::EXIST_P1) {
                trace!(%path, "modified (infer p2 modified)");
                modified.push(path);
            } else {
                // Since pending changes is with respect to P1, then if it's not in P1
                // we either saw it in the pending changes loop earlier (in which case
                // it is in `seen` and was handled), or we didn't see it and therefore
                // it doesn't exist and is either deleted or removed.
                if state.state.contains(StateFlags::EXIST_NEXT) {
                    trace!(%path, "deleted (in p2, in next, not in pending changes)");
                    deleted.push(path);
                } else {
                    trace!(%path, "removed (in p2, not in next, not in pending changes)");
                    removed.push(path);
                }
            }
            Ok(())
        },
    )?;

    // Files that will be removed (that is, they exist in either of the parents, but don't
    // exist in the next commit) should be marked as removed, even if they're not in
    // pending changes (e.g. even if the file still exists). Files that are in P2 but
    // not P1 are handled above, so we only need to handle files in P1 here.
    walk_treestate(
        &mut treestate,
        matcher.clone(),
        StateFlags::EXIST_P1,
        StateFlags::EXIST_NEXT,
        |path, state| {
            trace!(%path, "removed (in p1, not in next, not in pending changes)");
            removed.push(path);
            Ok(())
        },
    )?;

    // Handle "retroactive copies": when a clean file is marked as having been copied
    // from another file. These files should be marked as "modified".
    walk_treestate(
        &mut treestate,
        matcher.clone(),
        StateFlags::COPIED,
        StateFlags::empty(),
        |path, state| {
            trace!(%path, "modified (marked copy, not in pending changes)");
            modified.push(path);
            Ok(())
        },
    )?;

    Ok(StatusBuilder::new()
        .modified(modified)
        .added(added)
        .removed(removed)
        .deleted(deleted)
        .unknown(unknown)
        .invalid_path(invalid_path)
        .invalid_type(invalid_type))
}

#[cfg(test)]
mod tests {
    use status::FileStatus;
    use status::Status;
    use tempdir::TempDir;
    use treestate::filestate::FileStateV2;
    use types::RepoPath;
    use types::RepoPathBuf;
    const EXIST_P1: StateFlags = StateFlags::EXIST_P1;
    const EXIST_P2: StateFlags = StateFlags::EXIST_P2;
    const EXIST_NEXT: StateFlags = StateFlags::EXIST_NEXT;
    const COPIED: StateFlags = StateFlags::COPIED;

    use super::*;

    struct DummyManifest {
        files: Vec<RepoPathBuf>,
    }

    #[allow(unused_variables)]
    impl Manifest for DummyManifest {
        fn get(&self, path: &RepoPath) -> Result<Option<manifest::FsNodeMetadata>> {
            unimplemented!()
        }

        fn get_ignore_case(&self, path: &RepoPath) -> Result<Option<manifest::FsNodeMetadata>> {
            unimplemented!("get_ignore_case not implemented for StubCommit")
        }

        fn list(&self, path: &RepoPath) -> Result<manifest::List> {
            unimplemented!()
        }

        fn insert(
            &mut self,
            file_path: RepoPathBuf,
            file_metadata: manifest::FileMetadata,
        ) -> Result<()> {
            unimplemented!()
        }

        fn remove(&mut self, file_path: &RepoPath) -> Result<Option<manifest::FileMetadata>> {
            unimplemented!()
        }

        fn flush(&mut self) -> Result<types::HgId> {
            unimplemented!()
        }

        fn files<'a, M: 'static + Matcher + Sync + Send>(
            &'a self,
            matcher: M,
        ) -> Box<dyn Iterator<Item = Result<manifest::File>> + 'a> {
            Box::new(self.files.iter().cloned().map(|path| {
                Ok(manifest::File {
                    path,
                    meta: manifest::FileMetadata::default(),
                })
            }))
        }

        fn dirs<'a, M: 'static + Matcher + Sync + Send>(
            &'a self,
            matcher: M,
        ) -> Box<dyn Iterator<Item = Result<manifest::Directory>> + 'a> {
            unimplemented!()
        }

        fn diff<'a, M: Matcher>(
            &'a self,
            other: &'a Self,
            matcher: &'a M,
        ) -> Result<Box<dyn Iterator<Item = Result<manifest::DiffEntry>> + 'a>> {
            unimplemented!()
        }

        fn modified_dirs<'a, M: Matcher>(
            &'a self,
            other: &'a Self,
            matcher: &'a M,
        ) -> Result<Box<dyn Iterator<Item = Result<manifest::DirDiffEntry>> + 'a>> {
            unimplemented!()
        }
    }

    /// Compute the status with the given input.
    ///
    /// * `treestate` is a list of (path, state flags).
    /// * `changes` is a list of (path, deleted).
    fn status_helper(treestate: &[(&str, StateFlags)], changes: &[(&str, bool)]) -> Result<Status> {
        // Build the TreeState.
        let dir = TempDir::new("treestate").expect("tempdir");
        let mut state = TreeState::new(dir.path(), true).expect("open").0;
        let mut manifest_files = vec![];
        for (path, flags) in treestate {
            if *flags == (StateFlags::EXIST_P1 | StateFlags::EXIST_NEXT) {
                // Normal file, put it in the manifest instead of the TreeState.
                let path = RepoPathBuf::from_string(path.to_string()).expect("path");
                manifest_files.push(path);
            } else {
                let file_state = FileStateV2 {
                    mode: 0,
                    size: 0,
                    mtime: 0,
                    state: *flags,
                    copied: None,
                };
                state.insert(path, &file_state).expect("insert");
            }
        }
        let treestate = Arc::new(Mutex::new(state));
        let manifest = DummyManifest {
            files: manifest_files,
        };

        // Build the pending changes.
        let changes = changes.iter().map(|&(path, is_deleted)| {
            let path = RepoPathBuf::from_string(path.to_string()).expect("path");
            if is_deleted {
                Ok(ChangeType::Deleted(path))
            } else {
                Ok(ChangeType::Changed(path))
            }
        });

        // Compute the status.
        let matcher = Arc::new(pathmatcher::AlwaysMatcher::new());
        compute_status(&manifest, treestate, changes, matcher).map(StatusBuilder::build)
    }

    /// Compare the [`Status`] with the expected status for each given file.
    fn compare_status(status: Status, expected_list: &[(&str, Option<FileStatus>)]) {
        for (path, expected) in expected_list {
            let actual = status.status(RepoPath::from_str(path).expect("path"));
            assert_eq!(&actual, expected, "status for '{}'", path);
        }
    }

    /// Test status for files in pending changes.
    #[test]
    fn test_status_pending_changes() {
        let treestate = &[
            ("normal-file", EXIST_P1 | EXIST_NEXT),
            ("modified-file", EXIST_P1 | EXIST_NEXT),
            ("added-file", EXIST_NEXT),
            ("removed-file", EXIST_P1),
            ("deleted-file", EXIST_P1 | EXIST_NEXT),
        ];
        let changes = &[
            ("modified-file", false),
            ("added-file", false),
            ("removed-file", true),
            ("deleted-file", true),
            ("unknown-file", false),
        ];
        let status = status_helper(treestate, changes).expect("status");
        compare_status(
            status,
            &[
                ("normal-file", None),
                ("modified-file", Some(FileStatus::Modified)),
                ("added-file", Some(FileStatus::Added)),
                ("removed-file", Some(FileStatus::Removed)),
                ("deleted-file", Some(FileStatus::Deleted)),
                ("unknown-file", Some(FileStatus::Unknown)),
            ],
        );
    }

    /// Test status for files that aren't in pending changes.
    #[test]
    fn test_status_no_changes() {
        let treestate = &[
            ("added-then-deleted", EXIST_NEXT),
            ("removed-but-on-filesystem", EXIST_P1),
            ("retroactive-copy", EXIST_P1 | EXIST_NEXT | COPIED),
        ];
        let changes = &[];
        let status = status_helper(treestate, changes).expect("status");
        compare_status(
            status,
            &[
                ("added-then-deleted", Some(FileStatus::Deleted)),
                ("removed-but-on-filesystem", Some(FileStatus::Removed)),
                ("retroactive-copy", Some(FileStatus::Modified)),
            ],
        );
    }

    /// Test status for files relating to a merge.
    #[test]
    fn test_status_merge() {
        let treestate = &[
            ("merged-only-p2", EXIST_P2 | EXIST_NEXT),
            ("merged-in-both", EXIST_P1 | EXIST_P2 | EXIST_NEXT),
            ("merged-and-removed", EXIST_P2),
            ("merged-but-deleted", EXIST_P2 | EXIST_NEXT),
        ];
        let changes = &[("merged-only-p2", false), ("merged-in-both", false)];
        let status = status_helper(treestate, changes).expect("status");
        compare_status(
            status,
            &[
                ("merged-only-p2", Some(FileStatus::Modified)),
                ("merged-in-both", Some(FileStatus::Modified)),
                ("merged-and-removed", Some(FileStatus::Removed)),
                ("merged-but-deleted", Some(FileStatus::Deleted)),
            ],
        );
    }
}


/*
 * 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 pathmatcher::DirectoryMatch;
use pathmatcher::Matcher;
use treestate::filestate::FileStateV2;
use treestate::filestate::StateFlags;
use treestate::treestate::TreeState;
use types::path::ParseError;
use types::RepoPath;
use types::RepoPathBuf;

/// Walk the TreeState, calling the callback for files that have all flags in [`state_all`]
/// and none of the flags in [`state_none`]. Returns errors parsing invalid paths, if any.
pub fn walk_treestate(
    treestate: &mut TreeState,
    matcher: Arc<dyn Matcher + Send + Sync + 'static>,
    state_all: StateFlags,
    state_none: StateFlags,
    mut callback: impl FnMut(RepoPathBuf, &FileStateV2) -> Result<()>,
) -> Result<Vec<ParseError>> {
    let file_mask = state_all | state_none;
    let mut path_errors = Vec::new();

    treestate.visit(
        &mut |components, state| {
            match RepoPathBuf::from_utf8(components.concat()) {
                Ok(path) => {
                    if matcher.matches_file(&path)? {
                        (callback)(path, state)?
                    }
                }
                // Ingore but record bad paths. The caller can handle as desired.
                Err(parse_err) => path_errors.push(parse_err),
            };
            Ok(treestate::tree::VisitorResult::NotChanged)
        },
        &|components, dir| {
            let state_matches = match dir.get_aggregated_state() {
                Some(state) => {
                    state.union.contains(state_all) && !state.intersection.intersects(state_none)
                }
                None => true,
            };

            if !state_matches {
                return false;
            }

            if let Ok(dir_path) = RepoPath::from_utf8(&components.concat()) {
                if matches!(
                    matcher.matches_directory(dir_path),
                    Ok(DirectoryMatch::Nothing)
                ) {
                    return false;
                }
            }

            true
        },
        &|_path, file| file.state & file_mask == state_all,
    )?;

    Ok(path_errors)
}


/*
 * 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::HashSet;
use std::fs;
use std::fs::DirEntry;
use std::fs::Metadata;
use std::io;
use std::path::PathBuf;
use std::sync::atomic::AtomicU64;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::thread;
use std::thread::JoinHandle;
use std::time::Duration;

use anyhow::Error;
use anyhow::Result;
use crossbeam::channel::unbounded;
use crossbeam::channel::Receiver;
use crossbeam::channel::RecvError;
use crossbeam::channel::RecvTimeoutError;
use crossbeam::channel::Sender;
use pathmatcher::DirectoryMatch;
use pathmatcher::Matcher;
use thiserror::Error;
use types::path::ParseError;
use types::RepoPath;
use types::RepoPathBuf;

#[derive(Error, Debug)]
pub enum WalkError {
    #[error("invalid file name encoding '{0}'")]
    FsUtf8Error(String),
    #[error("IO error at '{0}': {1}")]
    IOError(RepoPathBuf, #[source] io::Error),
    #[error("path error at '{0}': {1}")]
    RepoPathError(String, #[source] ParseError),
    #[error("invalid file type at '{0}'")]
    InvalidFileType(RepoPathBuf),
    #[error("invalid mtime for '{0}': {1}")]
    InvalidMTime(RepoPathBuf, #[source] anyhow::Error),
    #[error("channel disconnected")]
    ChannelDisconnected(#[source] RecvTimeoutError),
    #[error("channel recv error")]
    ChannelRecvError(#[source] RecvError),
}

impl WalkError {
    pub fn filename(&self) -> String {
        match self {
            WalkError::FsUtf8Error(path) => path.to_string(),
            WalkError::IOError(path, _) => path.to_string(),
            WalkError::RepoPathError(path, _) => path.to_string(),
            WalkError::InvalidFileType(path) => path.to_string(),
            WalkError::InvalidMTime(path, _) => path.to_string(),
            WalkError::ChannelDisconnected(_) => "".to_string(),
            WalkError::ChannelRecvError(_) => "".to_string(),
        }
    }

    pub fn message(&self) -> String {
        match self {
            WalkError::FsUtf8Error(_) => "invalid file name encoding".to_string(),
            WalkError::IOError(_, error) => error.to_string(),
            WalkError::RepoPathError(_, error) => error.to_string(),
            WalkError::InvalidFileType(_) => "invalid file type".to_string(),
            WalkError::InvalidMTime(_, error) => format!("invalid mtime - {}", error.to_string()),
            WalkError::ChannelDisconnected(error) => error.to_string(),
            WalkError::ChannelRecvError(error) => error.to_string(),
        }
    }
}

pub enum WalkEntry {
    File(RepoPathBuf, Metadata),
    Directory(RepoPathBuf),
}

impl AsRef<RepoPath> for WalkEntry {
    fn as_ref(&self) -> &RepoPath {
        match self {
            WalkEntry::File(f, _) => f,
            WalkEntry::Directory(d) => d,
        }
    }
}

pub struct WalkerData<M> {
    result_sender: Sender<Result<WalkEntry>>,
    queue_sender: Sender<RepoPathBuf>,
    queue_receiver: Receiver<RepoPathBuf>,
    matcher: M,
    busy_nodes: AtomicU64,
    result_cnt: AtomicU64,
    root: PathBuf,
    include_directories: bool,
    dot_dir: String,
    skip_dirs: HashSet<RepoPathBuf>,
}

impl<M> WalkerData<M> {
    fn enqueue_result(&self, msg: Result<WalkEntry, Error>) -> Result<()> {
        self.result_cnt.fetch_add(1, Ordering::AcqRel);
        Ok(self.result_sender.send(msg)?)
    }

    fn enqueue_work(&self, msg: RepoPathBuf) -> Result<()> {
        self.busy_nodes.fetch_add(1, Ordering::AcqRel);
        Ok(self.queue_sender.send(msg)?)
    }
}

pub struct Walker<M> {
    threads: Vec<JoinHandle<Result<()>>>,
    results: Vec<Result<WalkEntry>>,
    result_receiver: Receiver<Result<WalkEntry>>,
    has_walked: bool,
    payload: Arc<WalkerData<M>>,
}

impl<M> Walker<M>
where
    M: Matcher,
    M: Clone,
    M: Send,
    M: Sync,
    M: 'static,
{
    const RECV_TIMEOUT: Duration = Duration::from_millis(5);

    pub fn new(
        root: PathBuf,
        dot_dir: String,
        skip_dirs: Vec<PathBuf>,
        matcher: M,
        include_directories: bool,
    ) -> Result<Self> {
        let (s_results, r_results) = unbounded();
        let (s_queue, r_queue) = unbounded();

        Ok(Walker {
            threads: Vec::with_capacity(8),
            results: Vec::new(),
            result_receiver: r_results,
            has_walked: false,
            payload: Arc::new(WalkerData {
                busy_nodes: AtomicU64::new(0),
                result_cnt: AtomicU64::new(0),
                result_sender: s_results,
                queue_sender: s_queue,
                queue_receiver: r_queue,
                root,
                matcher,
                include_directories,
                dot_dir,
                skip_dirs: skip_dirs
                    .into_iter()
                    .map(|p| Ok(p.try_into()?))
                    .collect::<Result<_>>()?,
            }),
        })
    }

    // WARNING: SIDE EFFECTS - if entry matches and is child directory, will push
    // child and increment busy_nodes atomic.
    fn match_entry_and_enqueue(
        dir: &RepoPathBuf,
        entry: DirEntry,
        shared_data: Arc<WalkerData<M>>,
    ) -> Result<()> {
        let filename = entry.file_name();
        let filename = filename
            .to_str()
            .ok_or_else(|| WalkError::FsUtf8Error(filename.to_string_lossy().into_owned()))?;
        let filename = RepoPath::from_str(filename)
            .map_err(|e| WalkError::RepoPathError(filename.to_owned(), e))?;
        let filetype = entry
            .file_type()
            .map_err(|e| WalkError::IOError(filename.to_owned(), e))?;

        let mut candidate_path = dir.clone();
        candidate_path.push(filename);
        if filetype.is_file() || filetype.is_symlink() {
            if shared_data
                .matcher
                .matches_file(candidate_path.as_repo_path())?
            {
                shared_data
                    .enqueue_result(Ok(WalkEntry::File(candidate_path, entry.metadata()?)))?;
            }
        } else if filetype.is_dir() {
            if !shared_data.skip_dirs.contains(filename)
                && shared_data
                    .matcher
                    .matches_directory(candidate_path.as_repo_path())?
                    != DirectoryMatch::Nothing
            {
                shared_data.enqueue_work(candidate_path)?;
            }
        } else if shared_data
            .matcher
            .matches_file(candidate_path.as_repo_path())?
        {
            return Err(WalkError::InvalidFileType(filename.to_owned()).into());
        }
        Ok(())
    }

    fn walk(&mut self) -> Result<()> {
        if self
            .payload
            .matcher
            .matches_directory(&RepoPathBuf::new())?
            != DirectoryMatch::Nothing
        {
            self.payload.enqueue_work(RepoPathBuf::new())?;
        }

        for _t in 0..self.threads.capacity() {
            let shared_data = self.payload.clone();

            // TODO make sure that _t is different for each thread
            self.threads.push(thread::spawn(move || {
                loop {
                    let result = shared_data
                        .queue_receiver
                        .recv_timeout(Walker::<M>::RECV_TIMEOUT);
                    match result {
                        Ok(dir) => {
                            // Anonymous function so we can capture all errors returned, and decrement
                            // busy_nodes even in the event of an error.
                            let result = (|| -> Result<()> {
                                if shared_data.include_directories {
                                    shared_data
                                        .enqueue_result(Ok(WalkEntry::Directory(dir.clone())))?;
                                }
                                let abs_dir_path = shared_data.root.join(dir.as_str());

                                // Skip nested repos.
                                if !dir.is_empty()
                                    && abs_dir_path.join(&shared_data.dot_dir).exists()
                                {
                                    return Ok(());
                                }

                                for entry in fs::read_dir(abs_dir_path)
                                    .map_err(|e| WalkError::IOError(dir.clone(), e))?
                                {
                                    let entry =
                                        entry.map_err(|e| WalkError::IOError(dir.clone(), e))?;
                                    if let Err(e) = Walker::match_entry_and_enqueue(
                                        &dir,
                                        entry,
                                        shared_data.clone(),
                                    ) {
                                        shared_data.enqueue_result(Err(e))?;
                                    }
                                }
                                Ok(())
                            })();
                            shared_data.busy_nodes.fetch_sub(1, Ordering::AcqRel);
                            result?;
                        }
                        Err(RecvTimeoutError::Timeout) => {
                            if shared_data.busy_nodes.load(Ordering::Relaxed) == 0 {
                                return Ok(());
                            }
                        }
                        Err(e) => {
                            return Err(WalkError::ChannelDisconnected(e).into());
                        }
                    };
                }
            }));
        }

        // TODO in future, let caller handle receiving on the channel.
        for handle in self.threads.drain(0..) {
            let thread_result = handle.join().expect("Failed to join thread.");
            if let Err(e) = thread_result {
                self.results.push(Err(e));
            }
        }
        let cnt = self.payload.result_cnt.load(Ordering::Relaxed);
        for _ in 0..cnt {
            let result = self.result_receiver.recv();

            match result {
                Ok(val) => self.results.push(val),
                // Should not get disconnected because Walker owns a Sender.
                Err(e) => return Err(WalkError::ChannelRecvError(e).into()),
            }
        }
        Ok(())
    }
}

impl<M> Iterator for Walker<M>
where
    M: Matcher,
    M: Clone,
    M: Send,
    M: Sync,
    M: 'static,
{
    type Item = Result<WalkEntry>;
    fn next(&mut self) -> Option<Self::Item> {
        if self.has_walked {
            self.results.pop()
        } else {
            self.has_walked = true;
            match self.walk() {
                Err(e) => Some(Err(e)),
                Ok(()) => self.results.pop(),
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use std::fs::create_dir_all;
    use std::fs::OpenOptions;
    use std::path::PathBuf;

    use pathmatcher::AlwaysMatcher;
    use pathmatcher::NeverMatcher;
    use pathmatcher::TreeMatcher;
    use tempfile::tempdir;

    use super::*;

    fn create_directory(
        directories: &std::vec::Vec<&str>,
        files: &std::vec::Vec<&str>,
    ) -> Result<tempfile::TempDir> {
        let root = tempdir()?;
        for dir in directories {
            create_dir_all(root.path().join(dir))?;
        }
        for file in files {
            let path = root.path().join(file);
            OpenOptions::new()
                .create(true)
                .write(true)
                .open(path.as_path())?;
        }
        Ok(root)
    }

    #[test]
    fn test_multiwalker_nevermatcher() -> Result<()> {
        let directories = vec!["dirA"];
        let files = vec!["dirA/a.txt", "b.txt"];
        let root_dir = create_directory(&directories, &files)?;
        let root_path = PathBuf::from(root_dir.path());
        let walker = Walker::new(
            root_path,
            ".hg".to_string(),
            Vec::new(),
            NeverMatcher::new(),
            false,
        )?;
        let walked_files: Result<Vec<_>> = walker.collect();
        let walked_files = walked_files?;
        assert!(walked_files.is_empty());
        Ok(())
    }

    #[test]
    fn test_multiwalker_treematcher() -> Result<()> {
        let directories = vec!["foo", "foo/bar"];
        let files = vec!["foo/cat.txt", "foo/bar/baz.txt"];
        let root_dir = create_directory(&directories, &files)?;
        let root_path = PathBuf::from(root_dir.path());
        let walker = Walker::new(
            root_path,
            ".hg".to_string(),
            Vec::new(),
            TreeMatcher::from_rules(["foo/bar/**"].iter(), true).unwrap(),
            false,
        )?;
        let walked_files: Result<Vec<_>> = walker.collect();
        let walked_files = walked_files?;
        let res = vec!["foo/bar/baz.txt"];
        assert_eq!(walked_files.len(), res.len());
        for file in walked_files {
            assert!(res.contains(&file.as_ref().to_string().as_str()));
        }
        Ok(())
    }

    #[test]
    fn test_multiwalker_dirs() -> Result<()> {
        let directories = vec!["dirA", "dirB/dirC/dirD"];
        let files = vec!["dirA/a.txt", "dirA/b.txt", "dirB/dirC/dirD/c.txt"];
        let root_dir = create_directory(&directories, &files)?;
        let root_path = PathBuf::from(root_dir.path());
        let walker = Walker::new(
            root_path,
            ".hg".to_string(),
            Vec::new(),
            AlwaysMatcher::new(),
            true,
        )?;
        let walked_files: Result<Vec<_>> = walker.collect();
        let walked_files = walked_files?;
        // Includes root dir ""
        let res = vec![
            "",
            "dirA",
            "dirA/a.txt",
            "dirA/b.txt",
            "dirB",
            "dirB/dirC",
            "dirB/dirC/dirD",
            "dirB/dirC/dirD/c.txt",
        ];
        assert_eq!(walked_files.len(), res.len());
        for file in walked_files {
            assert!(res.contains(&file.as_ref().to_string().as_str()));
        }
        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::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::SystemTime;

use anyhow::anyhow;
use anyhow::Context;
use anyhow::Result;
use configmodel::Config;
use configmodel::ConfigExt;
use identity::Identity;
use io::IO;
use manifest::FileType;
use manifest::Manifest;
use manifest_tree::ReadTreeManifest;
use manifest_tree::TreeManifest;
use parking_lot::Mutex;
use parking_lot::RwLock;
use pathmatcher::AlwaysMatcher;
use pathmatcher::DifferenceMatcher;
use pathmatcher::GitignoreMatcher;
use pathmatcher::IntersectMatcher;
use pathmatcher::Matcher;
use pathmatcher::UnionMatcher;
use repolock::RepoLocker;
use status::FileStatus;
use status::Status;
use status::StatusBuilder;
use storemodel::ReadFileContents;
use treestate::filestate::StateFlags;
use treestate::tree::VisitorResult;
use treestate::treestate::TreeState;
use types::repo::StorageFormat;
use types::HgId;
use types::RepoPath;
use types::RepoPathBuf;
use vfs::VFS;

#[cfg(feature = "eden")]
use crate::edenfs::EdenFileSystem;
use crate::errors;
use crate::filesystem::ChangeType;
use crate::filesystem::FileSystemType;
use crate::filesystem::PendingChangeResult;
use crate::filesystem::PendingChanges;
use crate::git::parse_submodules;
use crate::physicalfs::PhysicalFileSystem;
use crate::status::compute_status;
use crate::util::walk_treestate;
use crate::watchmanfs::WatchmanFileSystem;

type ArcReadFileContents = Arc<dyn ReadFileContents<Error = anyhow::Error> + Send + Sync>;
type ArcReadTreeManifest = Arc<dyn ReadTreeManifest + Send + Sync>;

struct FileSystem {
    vfs: VFS,
    file_store: ArcReadFileContents,
    file_system_type: FileSystemType,
    inner: Box<dyn PendingChanges + Send>,
}

impl AsRef<Box<dyn PendingChanges + Send>> for FileSystem {
    fn as_ref(&self) -> &Box<dyn PendingChanges + Send> {
        &self.inner
    }
}

pub struct WorkingCopy {
    vfs: VFS,
    ident: Identity,
    format: StorageFormat,
    treestate: Arc<Mutex<TreeState>>,
    tree_resolver: ArcReadTreeManifest,
    filesystem: Mutex<FileSystem>,
    ignore_matcher: Arc<GitignoreMatcher>,
    locker: Arc<RepoLocker>,
    dot_hg_path: PathBuf,
}

impl WorkingCopy {
    pub fn new(
        vfs: VFS,
        format: StorageFormat,
        // TODO: Have constructor figure out FileSystemType
        file_system_type: FileSystemType,
        treestate: Arc<Mutex<TreeState>>,
        tree_resolver: ArcReadTreeManifest,
        filestore: ArcReadFileContents,
        config: &dyn Config,
        locker: Arc<RepoLocker>,
    ) -> Result<Self> {
        tracing::debug!(target: "dirstate_size", dirstate_size=treestate.lock().len());

        let ignore_matcher = Arc::new(GitignoreMatcher::new(
            vfs.root(),
            WorkingCopy::global_ignore_paths(vfs.root(), config)
                .iter()
                .map(|i| i.as_path())
                .collect(),
            vfs.case_sensitive(),
        ));

        let filesystem = Mutex::new(Self::construct_file_system(
            vfs.clone(),
            file_system_type,
            treestate.clone(),
            tree_resolver.clone(),
            filestore,
            locker.clone(),
        )?);

        let root = vfs.root();
        let ident = match identity::sniff_dir(&root)? {
            Some(ident) => ident,
            None => {
                return Err(errors::RepoNotFound(root.to_string_lossy().to_string()).into());
            }
        };
        let dot_hg_path = vfs.join(RepoPath::from_str(ident.dot_dir())?);

        Ok(WorkingCopy {
            vfs,
            format,
            ident,
            treestate,
            tree_resolver,
            filesystem,
            ignore_matcher,
            locker,
            dot_hg_path,
        })
    }

    /// Working copy root path, with `.hg`.
    pub fn dot_hg_path(&self) -> &Path {
        &self.dot_hg_path
    }

    pub fn lock(&self) -> Result<repolock::RepoLockHandle, repolock::LockError> {
        self.locker.lock_working_copy(self.dot_hg_path.clone())
    }

    pub fn ensure_locked(&self) -> Result<(), repolock::LockError> {
        self.locker.ensure_working_copy_locked(&self.dot_hg_path)
    }

    pub fn treestate(&self) -> Arc<Mutex<TreeState>> {
        self.treestate.clone()
    }

    pub fn vfs(&self) -> &VFS {
        &self.vfs
    }

    pub fn parents(&self) -> Result<Vec<HgId>> {
        self.treestate.lock().parents().collect()
    }

    pub fn set_parents(&mut self, parents: &mut dyn Iterator<Item = &HgId>) -> Result<()> {
        self.treestate.lock().set_parents(parents)
    }

    pub(crate) fn current_manifests(
        treestate: &TreeState,
        tree_resolver: &ArcReadTreeManifest,
    ) -> Result<Vec<Arc<RwLock<TreeManifest>>>> {
        let mut parents = treestate.parents().peekable();
        if parents.peek_mut().is_some() {
            parents
                .into_iter()
                .map(|p| tree_resolver.get(&p?))
                .collect()
        } else {
            let null_commit = HgId::null_id().clone();
            Ok(vec![
                tree_resolver
                    .get(&null_commit)
                    .context("resolving null commit tree")?,
            ])
        }
    }

    fn global_ignore_paths(root: &Path, config: &dyn Config) -> Vec<PathBuf> {
        config
            .keys_prefixed("ui", "ignore.")
            .iter()
            .chain(Some(&"ignore".into()))
            .filter_map(
                |name| match config.get_nonempty_opt::<PathBuf>("ui", name) {
                    Ok(Some(path)) => Some(root.join(path)),
                    _ => None,
                },
            )
            .collect()
    }

    fn construct_file_system(
        vfs: VFS,
        file_system_type: FileSystemType,
        treestate: Arc<Mutex<TreeState>>,
        tree_resolver: ArcReadTreeManifest,
        store: ArcReadFileContents,
        locker: Arc<RepoLocker>,
    ) -> Result<FileSystem> {
        let inner: Box<dyn PendingChanges + Send> = match file_system_type {
            FileSystemType::Normal => Box::new(PhysicalFileSystem::new(
                vfs.clone(),
                tree_resolver,
                store.clone(),
                treestate.clone(),
                false,
            )?),
            FileSystemType::Watchman => Box::new(WatchmanFileSystem::new(
                vfs.clone(),
                treestate.clone(),
                tree_resolver,
                store.clone(),
                locker,
            )?),
            FileSystemType::Eden => {
                #[cfg(not(feature = "eden"))]
                panic!("cannot use EdenFS in a non-EdenFS build");
                #[cfg(feature = "eden")]
                Box::new(EdenFileSystem::new(vfs.clone())?)
            }
        };
        Ok(FileSystem {
            vfs,
            file_store: store,
            file_system_type,
            inner,
        })
    }

    fn added_files(&self) -> Result<Vec<RepoPathBuf>> {
        let mut added_files: Vec<RepoPathBuf> = vec![];
        self.treestate.lock().visit(
            &mut |components, _| {
                let path = components.concat();
                let path = RepoPathBuf::from_utf8(path)?;
                added_files.push(path);
                Ok(VisitorResult::NotChanged)
            },
            &|_path, dir| match dir.get_aggregated_state() {
                None => true,
                Some(state) => {
                    let any_not_exists_parent = !state
                        .intersection
                        .intersects(StateFlags::EXIST_P1 | StateFlags::EXIST_P2);
                    let any_exists_next = state.union.intersects(StateFlags::EXIST_NEXT);
                    any_not_exists_parent && any_exists_next
                }
            },
            &|_path, file| {
                !file
                    .state
                    .intersects(StateFlags::EXIST_P1 | StateFlags::EXIST_P2)
                    && file.state.intersects(StateFlags::EXIST_NEXT)
            },
        )?;
        Ok(added_files)
    }

    fn sparse_matcher(
        &self,
        manifests: &Vec<Arc<RwLock<TreeManifest>>>,
    ) -> Result<Arc<dyn Matcher + Send + Sync + 'static>> {
        let fs = &self.filesystem.lock();

        let mut sparse_matchers: Vec<Arc<dyn Matcher + Send + Sync + 'static>> = Vec::new();
        if fs.file_system_type == FileSystemType::Eden {
            sparse_matchers.push(Arc::new(AlwaysMatcher::new()));
        } else {
            for manifest in manifests.iter() {
                match crate::sparse::repo_matcher(
                    &self.vfs,
                    &fs.vfs.root().join(self.ident.dot_dir()),
                    manifest.read().clone(),
                    fs.file_store.clone(),
                )? {
                    Some((matcher, _hash)) => {
                        sparse_matchers.push(matcher);
                    }
                    None => {
                        sparse_matchers.push(Arc::new(AlwaysMatcher::new()));
                    }
                };
            }
        }

        Ok(Arc::new(UnionMatcher::new(sparse_matchers)))
    }

    pub fn status(
        &self,
        matcher: Arc<dyn Matcher + Send + Sync + 'static>,
        last_write: SystemTime,
        config: &dyn Config,
        io: &IO,
    ) -> Result<Status> {
        let added_files = self.added_files()?;

        let manifests =
            WorkingCopy::current_manifests(&self.treestate.lock(), &self.tree_resolver)?;
        let mut manifest_matchers: Vec<Arc<dyn Matcher + Send + Sync + 'static>> =
            Vec::with_capacity(manifests.len());

        let case_sensitive = self.vfs.case_sensitive();

        for manifest in manifests.iter() {
            manifest_matchers.push(Arc::new(manifest_tree::ManifestMatcher::new(
                manifest.clone(),
                case_sensitive,
            )));
        }

        let matcher = Arc::new(IntersectMatcher::new(vec![
            matcher,
            self.sparse_matcher(&manifests)?,
        ]));

        // The GitignoreMatcher minus files in the repo. In other
        // words, it does not match an ignored file that has been
        // previously committed.
        let ignore_matcher = Arc::new(DifferenceMatcher::new(
            self.ignore_matcher.clone(),
            UnionMatcher::new(manifest_matchers),
        ));

        let matcher = Arc::new(DifferenceMatcher::new(matcher, ignore_matcher.clone()));

        let mut ignore_dirs = vec![PathBuf::from(self.ident.dot_dir())];
        if self.format.is_git() {
            // Ignore file within submodules. Python has some logic additional
            // logic layered on top to add submodule info into status results.
            let git_modules_path = self.vfs.join(".gitmodules".try_into()?);
            if git_modules_path.exists() {
                ignore_dirs.extend(
                    parse_submodules(&util::file::read(&git_modules_path)?)?
                        .into_iter()
                        .map(|s| PathBuf::from(s.path)),
                );
            }
        }

        let pending_changes = self
            .filesystem
            .lock()
            .inner
            .pending_changes(
                matcher.clone(),
                ignore_matcher,
                ignore_dirs,
                last_write,
                config,
                io,
            )?
            .filter_map(|result| match result {
                Ok(PendingChangeResult::File(change_type)) => {
                    match matcher.matches_file(change_type.get_path()) {
                        Ok(true) => {
                            tracing::trace!(?change_type, "pending change");
                            Some(Ok(change_type))
                        }
                        Err(e) => Some(Err(e)),
                        _ => None,
                    }
                }
                Err(e) => Some(Err(e)),
                _ => None,
            })
            // fs.pending_changes() won't return ignored files, but we want added ignored files to
            // show up in the results, so let's inject them here.
            .chain(added_files.into_iter().filter_map(|path| {
                match self.ignore_matcher.matches_file(&path) {
                    Ok(result) if result => match self.vfs.metadata(&path) {
                        Ok(ref attr) if attr.is_dir() => None,
                        Ok(_) => Some(Ok(ChangeType::Changed(path))),
                        Err(_) => None,
                    },
                    Ok(_) => None,
                    Err(e) => Some(Err(e)),
                }
            }));

        let p1_manifest = &*manifests[0].read();
        let mut status_builder = compute_status(
            p1_manifest,
            self.treestate.clone(),
            pending_changes,
            matcher.clone(),
        )?;

        if !self.vfs.supports_symlinks()
            && config
                .get_or_default("unsafe", "filtersuspectsymlink")
                .unwrap_or_default()
        {
            status_builder =
                self.filter_accidential_symlink_changes(status_builder, p1_manifest)?;
        }

        Ok(status_builder.build())
    }

    // Filter out modified symlinks where it appears the symlink has
    // been modified to no longer be a symlink. This happens often on
    // Windows because we don't materialize symlinks in the working
    // copy. The comment in Python's _filtersuspectsymlink suggests it
    // can also happen on network mounts.
    fn filter_accidential_symlink_changes(
        &self,
        mut status_builder: StatusBuilder,
        manifest: &impl Manifest,
    ) -> Result<StatusBuilder> {
        let mut override_clean = Vec::new();
        for (path, status) in status_builder.iter() {
            if status != FileStatus::Modified {
                continue;
            }

            let file_metadata = match manifest.get_file(path)? {
                Some(md) => md,
                None => continue,
            };

            if file_metadata.file_type != FileType::Symlink {
                continue;
            }

            let data = self.vfs.read(path)?;
            if data.is_empty() || data.len() >= 1024 || data.iter().any(|b| *b == b'\n' || *b == 0)
            {
                override_clean.push(path.to_owned());
            }
        }

        if !override_clean.is_empty() {
            status_builder = status_builder.clean(override_clean)
        }

        Ok(status_builder)
    }

    pub fn copymap(
        &self,
        matcher: Arc<dyn Matcher + Send + Sync + 'static>,
    ) -> Result<Vec<(RepoPathBuf, RepoPathBuf)>> {
        let mut copied: Vec<(RepoPathBuf, RepoPathBuf)> = Vec::new();

        walk_treestate(
            &mut self.treestate.lock(),
            matcher,
            StateFlags::COPIED,
            StateFlags::empty(),
            |path, state| {
                let copied_path = state
                    .copied
                    .clone()
                    .ok_or_else(|| anyhow!("Invalid treestate entry for {}: missing copied from path on file with COPIED flag", path))
                    .map(|p| p.into_vec())
                    .and_then(|p| RepoPathBuf::from_utf8(p).map_err(|e| anyhow!(e)))?;

                copied.push((path, copied_path));

                Ok(())
            },
        )?;

        Ok(copied)
    }
}


/*
 * 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 pendingchanges;

pub use pendingchanges::ChangeType;
pub use pendingchanges::PendingChangeResult;
pub use pendingchanges::PendingChanges;

#[derive(PartialEq)]
pub enum FileSystemType {
    Normal,
    Watchman,
    Eden,
}


/*
 * 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::path::PathBuf;
use std::sync::Arc;
use std::time::SystemTime;

use anyhow::Result;
use configmodel::Config;
use io::IO;
use pathmatcher::Matcher;
use serde::Serialize;
use types::RepoPathBuf;

#[derive(Debug, Serialize)]
pub enum ChangeType {
    Changed(RepoPathBuf),
    Deleted(RepoPathBuf),
}

impl ChangeType {
    pub fn get_path(&self) -> &RepoPathBuf {
        match self {
            ChangeType::Changed(path) => path,
            ChangeType::Deleted(path) => path,
        }
    }
}

#[derive(Serialize)]
pub enum PendingChangeResult {
    File(ChangeType),
    SeenDirectory(RepoPathBuf),
}

pub trait PendingChanges {
    fn pending_changes(
        &self,
        // The full matcher including user specified filters.
        matcher: Arc<dyn Matcher + Send + Sync + 'static>,
        // Git ignore matcher, except won't match committed files.
        ignore_matcher: Arc<dyn Matcher + Send + Sync + 'static>,
        // Directories to always ignore such as ".sl".
        ignore_dirs: Vec<PathBuf>,
        last_write: SystemTime,
        config: &dyn Config,
        io: &IO,
    ) -> Result<Box<dyn Iterator<Item = Result<PendingChangeResult>>>>;
}


/*
 * 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.
 */

#[cfg(test)]
mod tests;
mod treestate;
mod watchmanfs;

pub use watchmanfs::WatchmanFileSystem;


/*
 * 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 pathmatcher::AlwaysMatcher;
use pathmatcher::Matcher;
use pathmatcher::NeverMatcher;
use treestate::filestate::FileStateV2;
use treestate::filestate::StateFlags;
use treestate::treestate::TreeState;
use types::RepoPathBuf;

use super::watchmanfs::detect_changes;
use crate::filechangedetector::FileChangeDetectorTrait;
use crate::filechangedetector::ResolvedFileChangeResult;
use crate::filesystem::ChangeType;
use crate::filesystem::PendingChangeResult;
use crate::metadata;

const NEED_CHECK: StateFlags = StateFlags::NEED_CHECK;
const EXIST_P1: StateFlags = StateFlags::EXIST_P1;
const EXIST_NEXT: StateFlags = StateFlags::EXIST_NEXT;

#[derive(Default)]
struct TestFileChangeDetector {
    changed_files: Vec<RepoPathBuf>,
    deleted_files: Vec<RepoPathBuf>,

    results: Vec<Result<ResolvedFileChangeResult>>,
}

impl FileChangeDetectorTrait for TestFileChangeDetector {
    fn submit(&mut self, file: metadata::File) {
        if self.changed_files.contains(&file.path) {
            self.results
                .push(Ok(ResolvedFileChangeResult::Yes(ChangeType::Changed(
                    file.path,
                ))));
        } else if self.deleted_files.contains(&file.path) {
            self.results
                .push(Ok(ResolvedFileChangeResult::Yes(ChangeType::Deleted(
                    file.path,
                ))));
        } else {
            self.results
                .push(Ok(ResolvedFileChangeResult::No(file.path)));
        }
    }
}

impl IntoIterator for TestFileChangeDetector {
    type Item = Result<ResolvedFileChangeResult>;
    type IntoIter = std::vec::IntoIter<Self::Item>;

    fn into_iter(self) -> Self::IntoIter {
        self.results.into_iter()
    }
}

#[derive(PartialEq, Debug, Copy, Clone)]
enum Change {
    Changed,
    Deleted,
}

use Change::*;

#[derive(Debug)]
enum MatcherMode {
    Always,
    Never,
}

use MatcherMode::*;

#[derive(Debug)]
struct TestCase {
    // initial treestate state for this file
    state_before: Option<StateFlags>,
    // whether watchman reported this file to us
    wm_changed: bool,
    // watchman is_fresh_instance (i.e. watchman restarted)
    wm_fresh_instance: bool,
    // whether the file appears to have changed/deleted when we check disk
    disk_changed: Option<Change>,
    // expected treestate after the dust settles (None means should be deleted)
    state_after: Option<StateFlags>,
    // what kind of pending change should be produced, if any
    pending_change: Option<Change>,
    // matcher to limit output of status
    matcher: MatcherMode,
}

fn check(mut tc: TestCase) -> Result<()> {
    let dir = tempfile::tempdir()?;

    let mut ts = TreeState::new(dir.path(), false)?.0;
    let path = RepoPathBuf::from_string("some_path".to_string())?;

    if let Some(state_before) = tc.state_before.clone() {
        ts.insert(
            &path,
            &FileStateV2 {
                mode: 0,
                size: 0,
                mtime: 0,
                copied: None,
                state: state_before,
            },
        )?;
    }

    let mut stub_detector = TestFileChangeDetector::default();
    if let Some(disk_changed) = tc.disk_changed.take() {
        match disk_changed {
            Change::Changed => stub_detector.changed_files.push(path.clone()),
            Change::Deleted => stub_detector.deleted_files.push(path.clone()),
        }
    }

    let mut wm_changes = Vec::new();
    if tc.wm_changed {
        wm_changes.push(path.clone());
    }

    let matcher: Arc<dyn Matcher + Send + Sync + 'static> = match tc.matcher {
        Always => Arc::new(AlwaysMatcher::new()),
        Never => Arc::new(NeverMatcher::new()),
    };

    //
    // This is the code under test.
    //
    let mut changes = detect_changes(
        matcher,
        Arc::new(NeverMatcher::new()),
        stub_detector,
        &mut ts,
        wm_changes
            .into_iter()
            .map(|p| metadata::File {
                path: p,
                fs_meta: None,
                ts_state: None,
            })
            .collect(),
        tc.wm_fresh_instance,
        true,
    )?;

    changes.update_treestate(&mut ts)?;

    let state = ts.get(&path)?;

    if let Some(state_after) = tc.state_after.clone() {
        assert!(
            state.is_some(),
            "expected file to be in treestate {:?}",
            &tc
        );
        assert_eq!(state.unwrap().state, state_after, "{:?}", &tc);
    } else {
        assert!(
            state.is_none(),
            "expected file to not be in treestate {:?}",
            &tc
        );
    }

    let mut pending_changes: Vec<_> = changes.into_iter().collect();
    if let Some(want_change) = tc.pending_change.clone() {
        assert!(pending_changes.len() == 1, "{:?}", &tc);
        if !pending_changes.is_empty() {
            match pending_changes.pop().unwrap().unwrap() {
                PendingChangeResult::File(change) => match change {
                    ChangeType::Changed(got_path) => {
                        assert_eq!(path, got_path);
                        assert_eq!(want_change, Change::Changed);
                    }
                    ChangeType::Deleted(got_path) => {
                        assert_eq!(path, got_path);
                        assert_eq!(want_change, Change::Deleted);
                    }
                },
                PendingChangeResult::SeenDirectory(_) => assert!(false, "seen directory?"),
            }
        }
    } else {
        assert_eq!(pending_changes.len(), 0, "{:?}", &tc);
    }

    Ok(())
}

fn product(flags: &[StateFlags]) -> Vec<StateFlags> {
    let len = 1 << flags.len();
    let mut result = Vec::with_capacity(len);
    for bits in 0..len {
        let mut flag = StateFlags::empty();
        for (i, &f) in flags.iter().enumerate() {
            if (bits & (1 << i)) != 0 {
                flag |= f;
            }
        }
        result.push(flag);
    }
    result
}

#[test]
fn test_detect_changes() -> Result<()> {
    const NEED_CHECK: StateFlags = StateFlags::NEED_CHECK;
    const EXIST_P1: StateFlags = StateFlags::EXIST_P1;
    const EXIST_NEXT: StateFlags = StateFlags::EXIST_NEXT;

    let all_states = product(&[NEED_CHECK, EXIST_P1, EXIST_NEXT]);
    for state_before in all_states {
        for wm_changed in [true, false] {
            for disk_changed in [Some(Changed), Some(Deleted), None] {
                // Expected state_after and pending_changes
                let state_after = {
                    let mut s = state_before;
                    match (wm_changed, &disk_changed, s.contains(NEED_CHECK)) {
                        (true, None, _) | (_, None, true) => {
                            s -= NEED_CHECK;
                        }
                        (true, Some(_), _) => {
                            s |= NEED_CHECK;
                        }
                        _ => {}
                    }
                    s
                };
                let pending_change = if wm_changed || state_before.contains(NEED_CHECK) {
                    disk_changed
                } else {
                    None
                };
                // Normalize states to Option form.
                let state_before = if state_before.is_empty() {
                    None
                } else {
                    Some(state_before)
                };
                let state_after = if state_after.is_empty() {
                    None
                } else {
                    Some(state_after)
                };
                check(TestCase {
                    state_before,
                    wm_changed,
                    disk_changed,
                    state_after,
                    pending_change,
                    wm_fresh_instance: false,
                    matcher: Always,
                })?;
            }
        }
    }

    Ok(())
}

#[test]
fn test_detect_changes_fresh_instance() -> Result<()> {
    check(TestCase {
        state_before: Some(EXIST_P1 | EXIST_NEXT),
        wm_changed: false,
        wm_fresh_instance: true,
        disk_changed: Some(Deleted),
        state_after: Some(EXIST_P1 | EXIST_NEXT | NEED_CHECK),
        pending_change: Some(Deleted),
        matcher: Always,
    })?;

    check(TestCase {
        state_before: Some(NEED_CHECK),
        wm_changed: false,
        wm_fresh_instance: true,
        disk_changed: Some(Deleted),
        state_after: None,
        pending_change: Some(Deleted),
        matcher: Always,
    })?;

    Ok(())
}

#[test]
fn test_never_matcher() -> Result<()> {
    // Make sure a non-matching matcher doesn't mess up correctness of
    // what is recorded in treestate.

    check(TestCase {
        state_before: Some(EXIST_P1 | EXIST_NEXT),
        wm_changed: true,
        wm_fresh_instance: false,
        disk_changed: Some(Changed),
        state_after: Some(EXIST_P1 | EXIST_NEXT | NEED_CHECK),
        // Perhaps we don't need to produce the pending change, but it gets
        // filtered later by the matcher.
        pending_change: Some(Changed),
        matcher: Never,
    })?;
    check(TestCase {
        state_before: Some(EXIST_P1 | EXIST_NEXT | NEED_CHECK),
        wm_changed: false,
        wm_fresh_instance: false,
        disk_changed: Some(Changed),
        state_after: Some(EXIST_P1 | EXIST_NEXT | NEED_CHECK),
        pending_change: None,
        matcher: Never,
    })?;
    check(TestCase {
        state_before: None,
        wm_changed: true,
        wm_fresh_instance: false,
        disk_changed: Some(Changed),
        state_after: Some(NEED_CHECK),
        pending_change: Some(Changed),
        matcher: Never,
    })?;
    check(TestCase {
        state_before: None,
        wm_changed: true,
        wm_fresh_instance: true,
        disk_changed: Some(Changed),
        state_after: Some(NEED_CHECK),
        pending_change: Some(Changed),
        matcher: Never,
    })?;

    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::BTreeMap;
use std::path::Path;
use std::sync::Arc;

use anyhow::anyhow;
use anyhow::Result;
use pathmatcher::Matcher;
use repolock::RepoLocker;
use treestate::dirstate;
use treestate::filestate::FileStateV2;
use treestate::filestate::StateFlags;
use treestate::treestate::TreeState;
use treestate::ErrorKind;
use types::path::ParseError;
use types::RepoPathBuf;
use watchman_client::prelude::*;

use crate::util::walk_treestate;

pub fn mark_needs_check(ts: &mut TreeState, path: &RepoPathBuf) -> Result<bool> {
    let state = ts.get(path)?;
    let filestate = match state {
        Some(filestate) => {
            let filestate = filestate.clone();
            if filestate.state.intersects(StateFlags::NEED_CHECK) {
                tracing::trace!(%path, "already NEED_CHECK");
                // It's already marked need_check, so return early so we don't mutate the
                // treestate.
                return Ok(false);
            }
            tracing::trace!(%path, "marking NEED_CHECK");
            FileStateV2 {
                state: filestate.state | StateFlags::NEED_CHECK,
                ..filestate
            }
        }
        // The file is currently untracked
        None => {
            tracing::trace!(%path, "inserting NEED_CHECK");
            FileStateV2 {
                state: StateFlags::NEED_CHECK,
                mode: 0o666,
                size: -1,
                mtime: -1,
                copied: None,
            }
        }
    };
    ts.insert(path, &filestate)?;
    Ok(true)
}

pub fn clear_needs_check(ts: &mut TreeState, path: &RepoPathBuf) -> Result<bool> {
    let state = ts.get(path)?;
    if let Some(filestate) = state {
        let filestate = filestate.clone();
        if !filestate.state.intersects(StateFlags::NEED_CHECK) {
            tracing::trace!(%path, "already not NEED_CHECK");
            // It's already clear.
            return Ok(false);
        }
        let filestate = FileStateV2 {
            state: filestate.state & !StateFlags::NEED_CHECK,
            ..filestate
        };

        if filestate.state.is_empty() {
            // No other flags means it was ignored/untracked, but now we don't
            // care about it (either it was deleted, or we aren't tracking
            // ignored files anymore).
            tracing::trace!(%path, "empty after unsetting NEED_CHECK");
            ts.remove(path)?;
        } else {
            tracing::trace!(%path, "unsetting NEED_CHECK");
            ts.insert(path, &filestate)?;
        }
        return Ok(true);
    }
    Ok(false)
}

pub fn set_clock(ts: &mut TreeState, clock: Clock) -> Result<()> {
    let clock_string = match clock {
        Clock::Spec(ClockSpec::StringClock(string)) => Ok(string),
        clock => Err(anyhow!(
            "Watchman implementation only handles opaque string type. Got the following clock instead: {:?}",
            clock
        )),
    }?;

    ts.update_metadata(&[("clock".to_string(), Some(clock_string))])?;

    Ok(())
}

#[tracing::instrument(skip_all)]
pub fn maybe_flush_treestate(root: &Path, ts: &mut TreeState, locker: &RepoLocker) -> Result<()> {
    match dirstate::flush(root, ts, locker) {
        Ok(()) => Ok(()),
        // If the dirstate was changed before we flushed, that's ok. Let the other write win
        // since writes during status are just optimizations.
        Err(e) => match e.downcast_ref::<ErrorKind>() {
            Some(e) if *e == ErrorKind::TreestateOutOfDate => Ok(()),
            _ => Err(e),
        },
    }
}

#[tracing::instrument(skip_all)]
pub fn list_needs_check(
    ts: &mut TreeState,
    matcher: Arc<dyn Matcher + Send + Sync + 'static>,
) -> Result<(Vec<RepoPathBuf>, Vec<ParseError>)> {
    let mut needs_check = Vec::new();

    let parse_errs = walk_treestate(
        ts,
        matcher,
        StateFlags::NEED_CHECK,
        StateFlags::empty(),
        |path, _state| {
            needs_check.push(path);
            Ok(())
        },
    )?;

    Ok((needs_check, parse_errs))
}

pub fn get_clock(metadata: &BTreeMap<String, String>) -> Result<Option<Clock>> {
    Ok(metadata
        .get(&"clock".to_string())
        .map(|clock| Clock::Spec(ClockSpec::StringClock(clock.clone()))))
}

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

    use super::*;

    #[test]
    fn test_skip_ignored_files() -> Result<()> {
        // Show that we respect the matcher to skip treestate files we don't care about.

        let fs = FileStateV2 {
            mode: 0,
            size: 0,
            mtime: 0,
            state: StateFlags::NEED_CHECK,
            copied: None,
        };

        let dir = tempfile::tempdir()?;

        let mut ts = TreeState::new(dir.path(), false)?.0;
        ts.insert("include_me", &fs)?;
        ts.insert("ignore_me", &fs)?;

        let matcher = Arc::new(ExactMatcher::new(
            [RepoPath::from_str("include_me")?].iter(),
            false,
        ));

        let (needs_check, _) = list_needs_check(&mut ts, matcher)?;
        assert_eq!(
            needs_check,
            vec![RepoPathBuf::from_string("include_me".to_string())?],
        );

        Ok(())
    }
}