use super::*;
use crate::HashMap;
use crate::pristine::InodeMetadata;
use parking_lot::Mutex;
use std::sync::Arc;
use std::time::SystemTime;

#[derive(Debug, Clone)]
pub struct Memory(Arc<Mutex<Memory_>>);

#[derive(Debug)]
struct Memory_ {
    files: FileTree,
    last_modified: SystemTime,
}

#[derive(Debug, Default)]
struct FileTree {
    children: HashMap<String, Inode>,
}
#[derive(Debug)]
enum Inode {
    File {
        meta: InodeMetadata,
        last_modified: SystemTime,
        contents: Arc<Mutex<Vec<u8>>>,
    },
    Directory {
        meta: InodeMetadata,
        last_modified: SystemTime,
        children: FileTree,
    },
}

impl Default for Memory {
    fn default() -> Self {
        Memory(Arc::new(Mutex::new(Memory_ {
            files: FileTree::default(),
            last_modified: SystemTime::now(),
        })))
    }
}

impl Memory {
    pub fn new() -> Self {
        Self::default()
    }
    pub fn list_files(&self) -> Vec<String> {
        let m = self.0.lock();
        let mut result = Vec::new();
        let mut current_files = vec![(String::new(), &m.files)];
        let mut next_files = Vec::new();
        loop {
            if current_files.is_empty() {
                break;
            }
            for (path, tree) in current_files.iter() {
                for (name, inode) in tree.children.iter() {
                    let mut path = path.clone();
                    crate::path::push(&mut path, name);
                    match inode {
                        Inode::File { .. } => {
                            result.push(path);
                        }
                        Inode::Directory { children, .. } => {
                            result.push(path.clone());
                            next_files.push((path, children))
                        }
                    }
                }
            }
            std::mem::swap(&mut current_files, &mut next_files);
            next_files.clear();
        }
        result
    }

    pub fn add_file(&self, file: &str, file_contents: Vec<u8>) {
        let file_meta = InodeMetadata::new(0, false);
        let last = SystemTime::now();
        self.add_inode(
            file,
            Inode::File {
                meta: file_meta,
                last_modified: last,
                contents: Arc::new(Mutex::new(file_contents)),
            },
        )
    }

    pub fn add_dir(&self, file: &str) {
        let file_meta = InodeMetadata::new(0o100, true);
        let last = SystemTime::now();
        self.add_inode(
            file,
            Inode::Directory {
                meta: file_meta,
                last_modified: last,
                children: FileTree {
                    children: HashMap::default(),
                },
            },
        )
    }

    fn add_inode(&self, file: &str, inode: Inode) {
        let mut m = self.0.lock();
        let last = SystemTime::now();
        m.last_modified = last;
        debug!("add_inode {:?} {:?}", last, inode);
        let mut file_tree = &mut m.files;
        let file = file.split('/').filter(|c| !c.is_empty());
        let mut p = file.peekable();
        while let Some(f) = p.next() {
            if p.peek().is_some() {
                let entry = file_tree
                    .children
                    .entry(f.to_string())
                    .or_insert(Inode::Directory {
                        meta: InodeMetadata::new(0o100, true),
                        children: FileTree {
                            children: HashMap::default(),
                        },
                        last_modified: last,
                    });
                match *entry {
                    Inode::Directory {
                        ref mut children, ..
                    } => file_tree = children,
                    _ => panic!("Not a directory"),
                }
            } else {
                file_tree.children.insert(f.to_string(), inode);
                break;
            }
        }
    }
}

impl Memory_ {
    fn get_file(&self, file: &str) -> Option<&Inode> {
        debug!("get_file {:?}", file);
        debug!("repo = {:?}", self);
        let mut t = Some(&self.files);
        let mut inode = None;
        let it = file.split('/').filter(|c| !c.is_empty());
        for c in it {
            debug!("c = {:?}", c);
            inode = t.take().unwrap().children.get(c);
            debug!("inode = {:?}", inode);
            match inode {
                Some(Inode::Directory { children, .. }) => t = Some(children),
                _ => break,
            }
        }
        inode
    }

    fn get_file_mut<'a>(&'a mut self, file: &str) -> Option<&'a mut Inode> {
        debug!("get_file_mut {:?}", file);
        debug!("repo = {:?}", self);
        let mut t = Some(&mut self.files);
        let mut it = file.split('/').filter(|c| !c.is_empty()).peekable();
        self.last_modified = SystemTime::now();
        while let Some(c) = it.next() {
            debug!("c = {:?}", c);
            let inode_ = t.take().unwrap().children.get_mut(c);
            debug!("inode = {:?}", inode_);
            if it.peek().is_none() {
                return inode_;
            }
            match inode_ {
                Some(Inode::Directory { children, .. }) => t = Some(children),
                _ => return None,
            }
        }
        None
    }

    fn remove_path_(&mut self, path: &str) -> Option<Inode> {
        debug!("remove_path {:?}", path);
        debug!("repo = {:?}", self);
        self.last_modified = SystemTime::now();
        let mut t = Some(&mut self.files);
        let mut it = path.split('/').filter(|c| !c.is_empty());
        let mut c = it.next().unwrap();
        loop {
            debug!("c = {:?}", c);
            let next_c = it.next();
            let t_ = t.take().unwrap();
            let next_c = if let Some(next_c) = next_c {
                next_c
            } else {
                return t_.children.remove(c);
            };
            let inode = t_.children.get_mut(c);
            c = next_c;
            debug!("inode = {:?}", inode);
            match inode {
                Some(Inode::Directory { children, .. }) => t = Some(children),
                _ => return None,
            }
        }
    }
}

#[derive(Debug, Error)]
pub enum Error {
    #[error("Path not found: {path}")]
    NotFound { path: String },
}

impl WorkingCopyRead for Memory {
    type Error = Error;
    fn file_metadata(&self, file: &str) -> Result<InodeMetadata, Self::Error> {
        let m = self.0.lock();
        match m.get_file(file) {
            Some(Inode::Directory { meta, .. }) => Ok(*meta),
            Some(Inode::File { meta, .. }) => Ok(*meta),
            None => Err(Error::NotFound {
                path: file.to_string(),
            }),
        }
    }
    fn read_file(&self, file: &str, buffer: &mut Vec<u8>) -> Result<(), Self::Error> {
        let m = self.0.lock();
        match m.get_file(file) {
            Some(Inode::Directory { .. }) => panic!("Not a file: {:?}", file),
            Some(Inode::File { contents, .. }) => {
                buffer.extend(&contents.lock()[..]);
                Ok(())
            }
            None => Err(Error::NotFound {
                path: file.to_string(),
            }),
        }
    }
    fn modified_time(&self, file: &str) -> Result<std::time::SystemTime, Self::Error> {
        let m = self.0.lock();
        match m.get_file(file) {
            Some(Inode::Directory { last_modified, .. })
            | Some(Inode::File { last_modified, .. }) => Ok(*last_modified),
            _ => {
                debug!("modified_time not found {:?}", file);
                Ok(m.last_modified)
            }
        }
    }
}

impl WorkingCopy for Memory {
    fn create_dir_all(&self, file: &str) -> Result<(), Self::Error> {
        let not_already_exists = {
            let m = self.0.lock();
            m.get_file(file).is_none()
        };
        if not_already_exists {
            let last = SystemTime::now();
            self.add_inode(
                file,
                Inode::Directory {
                    meta: InodeMetadata::new(0o100, true),
                    children: FileTree {
                        children: HashMap::default(),
                    },
                    last_modified: last,
                },
            );
        }
        Ok(())
    }

    fn remove_path(&self, path: &str, _rec: bool) -> Result<(), Self::Error> {
        self.0.lock().remove_path_(path);
        Ok(())
    }

    fn rename(&self, old: &str, new: &str) -> Result<(), Self::Error> {
        debug!("rename {:?} to {:?}", old, new);
        let inode = {
            let mut m = self.0.lock();
            m.remove_path_(old)
        };
        if let Some(inode) = inode {
            self.add_inode(new, inode)
        }
        Ok(())
    }
    fn set_permissions(&self, file: &str, permissions: u16) -> Result<(), Self::Error> {
        debug!("set_permissions {:?}", file);
        let mut m = self.0.lock();
        match m.get_file_mut(file) {
            Some(Inode::File {
                meta,
                last_modified,
                ..
            }) => {
                *last_modified = SystemTime::now();
                *meta = InodeMetadata::new(permissions as usize & 0o100, false);
            }
            Some(Inode::Directory {
                meta,
                last_modified,
                ..
            }) => {
                *last_modified = SystemTime::now();
                *meta = InodeMetadata::new(permissions as usize & 0o100, true);
            }
            None => panic!("file not found: {:?}", file),
        }
        Ok(())
    }

    type Writer = Writer;
    fn write_file(&self, file: &str, _: crate::Inode) -> Result<Self::Writer, Self::Error> {
        let mut m = self.0.lock();
        if let Some(f) = m.get_file_mut(file) {
            if let Inode::File {
                contents,
                last_modified,
                ..
            } = f
            {
                *last_modified = SystemTime::now();
                contents.lock().clear();
                return Ok(Writer {
                    w: contents.clone(),
                });
            } else {
                unreachable!()
            }
        }
        std::mem::drop(m);
        let contents = Arc::new(Mutex::new(Vec::new()));
        let last_modified = SystemTime::now();
        debug!("add_inode {:?}", last_modified);
        self.add_inode(
            file,
            Inode::File {
                meta: InodeMetadata::new(0, false),
                contents: contents.clone(),
                last_modified,
            },
        );
        Ok(Writer { w: contents })
    }

    fn touch(&self, file: &str, time: std::time::SystemTime) -> Result<(), Self::Error> {
        let mut m = self.0.lock();
        if let Some(f) = m.get_file_mut(file) {
            if let Inode::File { last_modified, .. } = f {
                *last_modified = time
            } else {
                unreachable!()
            }
        }
        Ok(())
    }
}

pub struct Writer {
    w: Arc<Mutex<Vec<u8>>>,
}

impl std::io::Write for Writer {
    fn write(&mut self, b: &[u8]) -> Result<usize, std::io::Error> {
        self.w.lock().write(b)
    }
    fn flush(&mut self) -> Result<(), std::io::Error> {
        Ok(())
    }
}
