use std::{
    sync::{
        atomic::{AtomicBool, AtomicUsize, Ordering},
        Arc,
    },
    vec,
};
use infinitree::{
    fields::{Collection, Store, VersionedMap},
    Digest,
};
use crate::Entry;
#[derive(Debug)]
pub enum FsError<'a> {
    InvalidPath(Vec<&'a str>),
    NoSuchFileOrDirectory,
    InvalidFilesystem,
}
pub type Result<'a, T> = std::result::Result<T, FsError<'a>>;
type InnerTree = VersionedMap<Digest, Node>;
pub struct Tree(InnerTree, AtomicBool);
impl Default for Tree {
    fn default() -> Tree {
        let tree = Tree(InnerTree::default(), false.into());
        tree.insert_root().unwrap();
        tree
    }
}
impl Clone for Tree {
    fn clone(&self) -> Self {
        Tree(self.0.clone(), self.1.load(Ordering::SeqCst).into())
    }
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub enum Node {
    File {
        refs: Arc<AtomicUsize>,
        entry: Arc<Entry>,
    },
    Directory {
        entries: scc::HashMap<String, Digest>,
    },
}
impl Node {
    fn directory() -> Node {
        Node::Directory {
            entries: scc::HashMap::with_capacity(0),
        }
    }
    fn file(entry: Entry) -> Node {
        Node::File {
            refs: AtomicUsize::new(1).into(),
            entry: entry.into(),
        }
    }
    pub fn as_file(&self) -> Option<Arc<Entry>> {
        match self {
            Node::File { entry, .. } => Some(Arc::clone(entry)),
            _ => None,
        }
    }
    pub fn is_file(&self) -> bool {
        matches!(self, Node::File { .. })
    }
    pub fn is_dir(&self) -> bool {
        matches!(self, Node::Directory { .. })
    }
}
impl Tree {
    fn insert_root<'a>(&self) -> Result<'a, ()> {
        if self.0.get(&Digest::default()).is_none() {
            _ = self.0.insert(Digest::default(), Node::directory());
        }
        self.1.store(false, Ordering::SeqCst);
        Ok(())
    }
    pub fn insert_directory<'a>(&self, path: &'a str) -> Result<'a, ()> {
        let (noderef, _current, filename) = self.create_path_to_parent(path)?;
        self.add_empty_dir(&noderef, filename);
        Ok(())
    }
    pub fn insert_file<'a>(&self, path: &'a str, file: Entry) -> Result<'a, ()> {
        let (noderef, _current, filename) = self.create_path_to_parent(path)?;
        self.add_file(&noderef, filename, file);
        Ok(())
    }
    pub fn update_file<'a>(&self, path: &'a str, file: Entry) -> Result<'a, ()> {
        let file_ref = self.get_ref(path)?.ok_or(FsError::NoSuchFileOrDirectory)?;
        self.0
            .update_with(file_ref, |_| Node::file(file))
            .ok_or(FsError::InvalidFilesystem)?;
        Ok(())
    }
    pub fn remove<'a>(&self, path: &'a str) -> Result<'a, ()> {
        let (parent_ref, parent, to_delete) = self.path_to_parent(path)?;
        let stack = scc::Stack::default();
        if let Node::Directory { ref entries } = parent.as_ref() {
            let node_ref = entries
                .read(to_delete, |_, v| *v)
                .ok_or(FsError::NoSuchFileOrDirectory)?;
            stack.push((parent_ref, node_ref, to_delete.to_string()));
            while let Some(next) = stack.pop().as_deref() {
                let (parent_ref, node_ref, entry_name) = (next.0, next.1, next.2.to_string());
                let file_node = self.0.get(&node_ref);
                match file_node.as_deref() {
                    Some(Node::File { .. }) => {
                        self.remove_file(&parent_ref, &entry_name);
                        self.0.remove(node_ref);
                    }
                    Some(Node::Directory {
                        entries: ref inner_entries,
                    }) => {
                        if inner_entries.is_empty() {
                            self.remove_file(&parent_ref, &entry_name);
                            self.0.remove(node_ref);
                        } else {
                            stack.push((parent_ref, node_ref, entry_name.to_string()));
                            inner_entries.scan(|file, childref| {
                                stack.push((node_ref, *childref, file.to_string()));
                            });
                        }
                    }
                    None => return Err(FsError::InvalidFilesystem),
                }
            }
        }
        Ok(())
    }
    pub fn file<'a>(&self, path: &'a str) -> Result<'a, Option<Arc<Entry>>> {
        let Some(noderef) = self.get_ref(path)? else {
            return Ok(None);
        };
        let Some(node) = self.0.get(&noderef) else {
            return Err(FsError::InvalidFilesystem);
        };
        Ok(node.as_file())
    }
    pub fn node_by_ref(&self, noderef: &Digest) -> Option<Arc<Node>> {
        self.0.get(noderef)
    }
    pub fn node_by_path<'a>(&self, path: &'a str) -> Result<'a, Option<Arc<Node>>> {
        if path == "/" {
            return Ok(Some(self.root()));
        }
        let Some(noderef) = self.get_ref(path)? else {
            return Ok(None);
        };
        let Some(node) = self.0.get(&noderef) else {
            return Err(FsError::InvalidFilesystem);
        };
        Ok(Some(node))
    }
    pub fn move_node<'a>(&self, old_path: &'a str, new_path: &'a str) -> Result<'a, ()> {
        let (parent_ref, _, node_name) = self.path_to_parent(old_path)?;
        let noderef = {
            let mut noderef = None;
            self.0.update_with(parent_ref, |current| {
                let Node::Directory { ref entries } = current.as_ref() else {
                    unreachable!()
                };
                noderef = entries.remove(node_name);
                current
            });
            let Some((_, noderef)) = noderef else {
                return Err(FsError::NoSuchFileOrDirectory);
            };
            noderef
        };
        let (new_ref, _, new_node_name) = self.path_to_parent(new_path)?;
        self.0.update_with(new_ref, |new| {
            let Node::Directory { ref entries } = new.as_ref() else {
                unreachable!()
            };
            _ = entries.insert(new_node_name.into(), noderef);
            new
        });
        Ok(())
    }
    fn root(&self) -> Arc<Node> {
        self.0.get(&Digest::default()).expect("uninitialized tree")
    }
    fn remove_file(&self, parent_ref: &Digest, filename: &str) {
        self.0.update_with(*parent_ref, |new| {
            if let Node::Directory { ref entries } = new.as_ref() {
                entries.remove(filename);
                return new;
            }
            unreachable!()
        });
    }
    fn get_ref<'a>(&self, path: &'a str) -> Result<'a, Option<Digest>> {
        let (_, current, filename) = self.path_to_parent(path)?;
        let Node::Directory { ref entries } = current.as_ref() else {
            unreachable!()
        };
        Ok(entries.read(filename, |_, v| *v))
    }
    pub fn is_file<'a>(&self, path: &'a str) -> Result<'a, bool> {
        let (_, current, _filename) = self.path_to_parent(path)?;
        let Node::Directory { ref entries } = current.as_ref() else {
            unreachable!()
        };
        Ok(entries
            .read(path, |_, v| *v)
            .and_then(|key| self.0.get(&key))
            .map(|node| node.is_file())
            .unwrap_or(false))
    }
    fn path_to_parent<'a>(&self, path: &'a str) -> Result<'a, (Digest, Arc<Node>, &'a str)> {
        let mut parts = path
            .split('/')
            .filter(|s| !s.is_empty())
            .collect::<Vec<&'a str>>();
        if parts.is_empty() {
            return Err(FsError::InvalidPath(Vec::default()));
        }
        let mut current = Some(self.root());
        let mut current_ref = Some(Digest::default());
        let mut consumed = Vec::with_capacity(parts.len());
        let last_part = parts.pop().unwrap();
        for part in parts.iter() {
            consumed.push(*part);
            let Some(Node::Directory { entries }) = current.as_deref() else {
                return Err(FsError::InvalidPath(consumed));
            };
            current_ref = entries.read(*part, |_, v| *v);
            current = current_ref.and_then(|nodeid| self.0.get(&nodeid));
        }
        let parent = current.ok_or(FsError::InvalidPath(consumed))?;
        Ok((current_ref.expect("must be Some"), parent, last_part))
    }
    fn create_path_to_parent<'a>(&self, path: &'a str) -> Result<'a, (Digest, Arc<Node>, &'a str)> {
        let mut parts = path
            .split('/')
            .filter(|s| !s.is_empty())
            .collect::<Vec<&'a str>>();
        if parts.is_empty() {
            return Err(FsError::InvalidPath(Vec::default()));
        }
        let mut parent: Digest;
        let mut current = Some(self.root());
        let mut current_ref = Some(Digest::default());
        let mut consumed = Vec::with_capacity(parts.len());
        let last_part = parts.pop().unwrap();
        for part in parts.iter() {
            consumed.push(*part);
            let Some(Node::Directory { entries }) = current.as_deref() else {
                return Err(FsError::InvalidPath(consumed));
            };
            if let Some(new_current_ref) = entries.read(*part, |_, v| *v) {
                current_ref = Some(new_current_ref);
                current = current_ref.and_then(|noderef| self.0.get(&noderef));
            } else {
                parent = current_ref.expect("current is Some");
                let (new_current_ref, new_current) = self.add_empty_dir(&parent, part);
                current_ref = Some(new_current_ref);
                current = Some(new_current);
            }
        }
        let current = current.ok_or(FsError::InvalidPath(consumed))?;
        Ok((current_ref.expect("must be Some"), current, last_part))
    }
    fn add_empty_dir(&self, parent: &Digest, name: &str) -> (Digest, Arc<Node>) {
        let noderef: Digest = rand::random();
        self.0.update_with(*parent, |parent| {
            let Node::Directory { ref entries } = parent.as_ref() else {
                panic!("invalid use of library");
            };
            _ = entries.insert(name.into(), noderef);
            parent
        });
        self.0.insert(noderef, Node::directory());
        (noderef, self.0.get(&noderef).expect("just inserted"))
    }
    fn add_file(&self, parent: &Digest, name: &str, file: Entry) -> (Digest, Arc<Node>) {
        let mut noderef: Digest = rand::random();
        let mut update = false;
        self.0.update_with(*parent, |parent| {
            let Node::Directory { ref entries } = parent.as_ref() else {
                panic!("invalid use of library");
            };
            _ = entries.upsert(
                name.into(),
                move || noderef,
                |_, v| {
                    noderef = *v;
                    update = true;
                },
            );
            parent
        });
        let new_node = Arc::new(Node::file(file));
        if update {
            self.0.update_with(noderef, |_| new_node.clone());
        } else {
            self.0.insert(noderef, new_node.clone());
        }
        (noderef, new_node)
    }
    pub fn retain<F>(&self, mut f: F)
    where
        F: FnMut(&str, &Node) -> bool,
    {
        let stack = scc::Stack::default();
        stack.push((String::new(), Digest::default()));
        let mut to_remove = vec![];
        while let Some(next) = stack.pop() {
            let (path, noderef) = (next.0.to_string(), next.1);
            if let Some(node) = self.0.get(&noderef) {
                match node.as_ref() {
                    Node::File { refs: _, entry: _ } => {
                        if !f(&path, &node) {
                            to_remove.push(path);
                        }
                    }
                    Node::Directory { entries } => {
                        if !f(&path, &node) {
                            to_remove.push(path);
                        } else {
                            entries.scan(|relative_path, noderef| {
                                let full_path = if path.is_empty() {
                                    relative_path.clone()
                                } else {
                                    format!("{}/{}", path, relative_path)
                                };
                                stack.push((full_path, *noderef));
                            });
                        }
                    }
                }
            }
        }
        for key in to_remove {
            _ = self.remove(&key);
        }
    }
    pub fn iter_files(&self) -> TreeIterator {
        let root = Arc::clone(&self.root());
        let stack = scc::Stack::default();
        stack.push((String::new(), root));
        TreeIterator { stack, inner: self }
    }
    pub fn clear(&self) -> usize {
        let count = self.0.clear();
        self.insert_root().expect("just cleared");
        count
    }
}
pub struct TreeIterator<'a> {
    stack: scc::Stack<(String, Arc<Node>)>,
    inner: &'a Tree,
}
impl<'a> Iterator for TreeIterator<'a> {
    type Item = (String, Arc<Entry>);
    fn next(&mut self) -> Option<Self::Item> {
        while let Some(next) = self.stack.pop() {
            let (prefix, node) = (next.0.to_string(), next.1.as_ref());
            match node {
                Node::File { refs: _, entry } => return Some((prefix, entry.clone())),
                Node::Directory { entries } => {
                    entries.scan(|name, digest| {
                        let path = if prefix.is_empty() {
                            name.to_string()
                        } else {
                            format!("{prefix}/{name}")
                        };
                        if let Some(curr) = self.inner.0.get(digest) {
                            self.stack.push((path, curr));
                        }
                    });
                }
            }
        }
        None
    }
}
impl Collection for Tree {
    type Depth = infinitree::fields::depth::Incremental;
    type Key = <InnerTree as Collection>::Key;
    type Serialized = <InnerTree as Collection>::Serialized;
    type Item = <InnerTree as Collection>::Item;
    fn key(from: &Self::Serialized) -> &Self::Key {
        <InnerTree as Collection>::key(from)
    }
    fn load(from: Self::Serialized, object: &mut dyn infinitree::object::Reader) -> Self::Item {
        <InnerTree as Collection>::load(from, object)
    }
    fn insert(&mut self, record: Self::Item) {
        static DIGEST: Digest = [0u8; 32];
        let (key, new_entry) = record;
        if !self.1.load(Ordering::Relaxed) && key == DIGEST && self.0.contains(&DIGEST) {
            self.0.update_with(key, |_| new_entry.expect("not-null"));
            self.1.store(true, Ordering::Relaxed);
        } else if !self.0.contains(&key) {
            <InnerTree as Collection>::insert(&mut self.0, (key, new_entry))
        }
    }
}
impl Store for Tree {
    fn store(
        &mut self,
        transaction: &mut dyn infinitree::index::Transaction,
        object: &mut dyn infinitree::object::Writer,
    ) {
        <InnerTree as Store>::store(&mut self.0, transaction, object)
    }
}
#[cfg(test)]
mod test {
    use std::assert_eq;
    use infinitree::{crypto::UsernamePassword, Digest, Infinitree};
    use scc::HashSet;
    use crate::{Entry, Files, Node, Tree};
    #[test]
    fn test_create_path_to_parent() {
        let tree = Tree::default();
        let path = "/test/path/to/dir";
        assert!(tree.path_to_parent(path).is_err());
        let res = tree.create_path_to_parent(path);
        assert!(res.is_ok());
        let (_, _, dir) = res.unwrap();
        assert_eq!("dir", dir);
        assert!(tree.path_to_parent(path).is_ok());
    }
    #[test]
    fn test_insert_and_remove_file() {
        let tree = Tree::default();
        let file_path = "test/path/to/file.rs";
        let res = tree.insert_file(file_path, Entry::default());
        assert!(res.is_ok());
        let res = tree.file(file_path);
        assert!(res.is_ok() && res.unwrap().is_some());
        assert!(tree.remove(file_path).is_ok());
        assert!(tree.file(file_path).unwrap().is_none());
    }
    #[test]
    fn test_move_node() {
        let key = || {
            UsernamePassword::with_credentials("bare_index_map".to_string(), "password".to_string())
                .unwrap()
        };
        let storage = crate::backends::test::InMemoryBackend::shared();
        let file_path = "test/path/file.rs".to_string();
        let random_file = "test/path/to/random.rs".to_string();
        let new_file_path = "test/path/to/file.rs".to_string();
        let entry = Entry {
            name: String::from("file.rs"),
            size: 1234,
            ..Default::default()
        };
        {
            let mut tree = Infinitree::<Files>::empty(storage.clone(), key()).unwrap();
            {
                let tree_index = &tree.index().tree;
                _ = tree_index.insert_file(&file_path, entry.clone());
                _ = tree_index.insert_file(&random_file, Entry::default());
                _ = tree_index.move_node(&file_path, &new_file_path);
                assert!(tree_index.file(&file_path).unwrap().is_none());
                assert_eq!(
                    tree_index.file(&new_file_path).unwrap().unwrap().as_ref(),
                    &entry
                );
            }
            tree.commit(None).unwrap();
            tree.index().tree.clear();
            tree.load_all().unwrap();
            {
                let tree_index = &tree.index().tree;
                assert!(tree_index.file(&file_path).unwrap().is_none());
                assert_eq!(
                    tree_index.file(&new_file_path).unwrap().unwrap().as_ref(),
                    &entry
                );
            }
            tree.commit(None).unwrap();
        }
        let tree = Infinitree::<Files>::open(storage, key()).unwrap();
        tree.load_all().unwrap();
        let tree_index = &tree.index().tree;
        assert!(tree_index.file(&file_path).unwrap().is_none());
        assert_eq!(
            tree_index.file(&new_file_path).unwrap().unwrap().as_ref(),
            &entry
        );
    }
    #[test]
    fn test_iterate_all_files() {
        let tree = Tree::default();
        let file1 = "test/path/to/file.rs".to_string();
        let file2 = "test/path/file2.rs".to_string();
        _ = tree.insert_file(
            &file1,
            Entry {
                name: "file.rs".into(),
                size: 1234,
                ..Default::default()
            },
        );
        _ = tree.insert_file(
            &file1,
            Entry {
                name: "file.rs".into(),
                size: 4321,
                ..Default::default()
            },
        );
        _ = tree.insert_file(
            &file2,
            Entry {
                name: "file2.rs".into(),
                ..Default::default()
            },
        );
        let files = HashSet::new();
        _ = files.insert(file1);
        _ = files.insert(file2);
        for (k, v) in tree.iter_files() {
            files.remove(&k).unwrap();
            match v.name.as_ref() {
                "file.rs" => assert_eq!(v.size, 4321),
                "file2.rs" => (),
                _ => panic!("invalid file"),
            }
        }
        assert_eq!(files.len(), 0);
    }
    #[test]
    fn test_update_file() {
        let key = || {
            UsernamePassword::with_credentials("bare_index_map".to_string(), "password".to_string())
                .unwrap()
        };
        let storage = crate::backends::test::InMemoryBackend::shared();
        let file_path = "test/path/file.rs".to_string();
        let entry = Entry {
            name: String::from("file.rs"),
            ..Default::default()
        };
        let new_entry = Entry {
            name: String::from("new_file.rs"),
            ..Default::default()
        };
        {
            let mut tree = Infinitree::<Files>::empty(storage.clone(), key()).unwrap();
            {
                let tree_index = &tree.index().tree;
                assert!(tree_index
                    .update_file(&file_path, new_entry.clone())
                    .is_err());
                tree_index.insert_file(&file_path, entry).unwrap();
                let entry_name = &tree_index.file(&file_path).unwrap().unwrap().name;
                assert_eq!(entry_name, "file.rs");
            }
            tree.commit(None).unwrap();
            tree.index().tree.clear();
            tree.load_all().unwrap();
            {
                let tree_index = &tree.index().tree;
                let entry_name = &tree_index.file(&file_path).unwrap().unwrap().name;
                assert_eq!(entry_name, "file.rs");
                let _ = tree_index.update_file(&file_path, new_entry);
                let entry_name = &tree_index.file(&file_path).unwrap().unwrap().name;
                assert_eq!(entry_name, "new_file.rs");
            }
            tree.commit(None).unwrap();
        }
        let tree = Infinitree::<Files>::open(storage, key()).unwrap();
        tree.load_all().unwrap();
        let tree_index = &tree.index().tree;
        let entry_name = &tree_index.file(&file_path).unwrap().unwrap().name;
        assert_eq!(entry_name, "new_file.rs");
    }
    #[test]
    fn test_index_can_be_restored() {
        let key = || {
            UsernamePassword::with_credentials("bare_index_map".to_string(), "password".to_string())
                .unwrap()
        };
        let storage = crate::backends::test::InMemoryBackend::shared();
        let file1 = "file1.rs".to_string();
        let file2 = "file2.rs".to_string();
        let file3 = "file3.rs".to_string();
        {
            let mut tree = Infinitree::<Files>::empty(storage.clone(), key()).unwrap();
            {
                let index = &tree.index().tree;
                index.insert_file(&file1, Entry::default()).unwrap();
                index.insert_file(&file2, Entry::default()).unwrap();
            }
            assert_eq!(tree.index().tree.0.len(), 3);
            tree.commit(None).unwrap();
            assert_eq!(3, tree.index().tree.clear());
            tree.load_all().unwrap();
            {
                let index = &tree.index().tree;
                index
                    .insert_file(
                        &file1,
                        Entry {
                            name: "file1.rs".into(),
                            ..Default::default()
                        },
                    )
                    .unwrap();
                assert_eq!(
                    &tree.index().tree.file(&file1).unwrap().unwrap().name,
                    "file1.rs"
                );
                index.insert_file(&file3, Entry::default()).unwrap();
            }
            tree.commit(None).unwrap();
        }
        let tree = Infinitree::<Files>::open(storage, key()).unwrap();
        tree.load_all().unwrap();
        assert_eq!(
            &tree.index().tree.file(&file1).unwrap().unwrap().name,
            "file1.rs"
        );
        assert_eq!(tree.index().tree.0.len(), 4);
    }
    #[test]
    fn test_iter_all_files_2() {
        let key = || {
            UsernamePassword::with_credentials("bare_index_map".to_string(), "password".to_string())
                .unwrap()
        };
        let storage = crate::backends::test::InMemoryBackend::shared();
        let file1 = "test/path/to/file.rs".to_string();
        let file2 = "test/path/file.rs".to_string();
        let file3 = "test/file.rs".to_string();
        {
            let mut tree = Infinitree::<Files>::empty(storage.clone(), key()).unwrap();
            {
                tree.index()
                    .tree
                    .insert_file(&file1, Entry::default())
                    .unwrap();
                tree.index()
                    .tree
                    .insert_file(&file2, Entry::default())
                    .unwrap();
                assert_eq!(tree.index().tree.iter_files().count(), 2);
            }
            tree.commit(None).unwrap();
            tree.index().tree.clear();
            tree.load_all().unwrap();
            {
                let _ = tree.index().tree.insert_file(&file3, Entry::default());
                assert_eq!(tree.index().tree.iter_files().count(), 3);
            }
            tree.commit(None).unwrap();
        }
        let tree = Infinitree::<Files>::open(storage, key()).unwrap();
        tree.load_all().unwrap();
        assert_eq!(tree.index().tree.iter_files().count(), 3);
    }
    #[test]
    fn test_get_dir() {
        let tree = Tree::default();
        let file = "test/path/to/file.rs".to_string();
        tree.insert_file(&file, Entry::default()).unwrap();
        let node = tree.node_by_path("test/path/to").unwrap().unwrap();
        assert!(node.is_dir());
    }
    #[test]
    fn test_path_to_folder() {
        let tree = Tree::default();
        let file = "home/travel/pic.png".to_string();
        tree.insert_file(&file, Entry::default()).unwrap();
        let root = tree.root();
        let (root_ref, root_node, test_name) = tree.path_to_parent("home").unwrap();
        assert_eq!(Digest::default(), root_ref);
        assert_eq!(root.is_dir(), root_node.is_dir());
        assert_eq!("home", test_name);
        if let Node::Directory { entries } = root.as_ref().clone() {
            let (test_ref, test_node, to_name) = tree.path_to_parent("home/travel").unwrap();
            let t_ref = entries.read("home", |_, v| *v).unwrap();
            let t_node = tree.0.get(&t_ref).unwrap();
            assert_eq!(t_ref, test_ref);
            assert_eq!(t_node.is_dir(), test_node.is_dir());
            assert_eq!("travel", to_name);
        }
    }
    #[test]
    fn test_remove_dir() {
        let tree = Tree::default();
        let file = "home/travel/pic.png".to_string();
        let file2 = "home/travel/dogs/dog.png".to_string();
        tree.insert_file(&file, Entry::default()).unwrap();
        tree.insert_file(&file2, Entry::default()).unwrap();
        assert!(tree.remove("home/travel").is_ok());
        assert!(tree.node_by_path("home/travel").unwrap().is_none());
    }
}