use bstr::ByteSlice;
use crate::common::{git::Git, lines::TypedLines};
use std::{collections::HashMap, ffi::OsStr, fs, io, io::Write, path::Path, sync::Arc};
use std::os::unix::{ffi::OsStrExt, fs::OpenOptionsExt, fs::PermissionsExt};

/// Direct access to the real file system.
#[derive(Default)]
pub(crate) struct RealFilesystem {}

/// Filesystem access routines. This transparently handles "virtual" files that are the result
/// of previously applied changes.
#[derive(Clone, derive_more::Deref)]
pub(crate) struct SharedVirtualFilesystem(Arc<VirtualFilesystem>);

pub(crate) struct VirtualFilesystem {
    pub(crate) downstream_repo: Git,
    virtual_files: HashMap<Vec<u8>, VirtualFile>,
}

struct VirtualFile {
    /// If Some, the rest of the fields are None and the file should be re-looked with the new
    /// name. None if not renamed. 
    new_path: Option<Vec<u8>>,
    /// The content of the file. None if the file was renamed or deleted.
    content: Option<Vec<u8>>,
}

/// Filesystem write functions. Implementers of this trait either store the changes in memory
/// (VirtualFilesystem) or directly execute them (RealFilesystem).
pub(crate) trait FilesystemWrite {
    fn exists(&self, path: &OsStr) -> io::Result<bool>;
    fn remove_file(&mut self, path: &OsStr) -> io::Result<()>;
    fn create_file<'a>(
        &mut self,
        path: &OsStr,
        rename_to: Option<&OsStr>,
        permissions: Option<fs::Permissions>,
        data: impl Iterator<Item = &'a [u8]>,
    ) -> io::Result<()>;
}

impl FilesystemWrite for RealFilesystem {
    fn exists(&self, path: &OsStr) -> io::Result<bool> {
        fs::exists(path)
    }

    fn remove_file(&mut self, path: &OsStr) -> io::Result<()> {
        fs::remove_file(path)
    }

    fn create_file<'a>(
        &mut self,
        path: &OsStr,
        rename_to: Option<&OsStr>,
        permissions: Option<fs::Permissions>,
        data: impl Iterator<Item = &'a [u8]>,
    ) -> io::Result<()> {
        if let Some(parent) = Path::new(path).parent() {
            fs::create_dir_all(parent)?;
        }
        let mut opts = fs::OpenOptions::new();
        opts.write(true)
            .truncate(true)
            .create(true);
        if let Some(permissions) = &permissions {
            opts.mode(permissions.mode());
        }
        let mut f = opts.open(path)?;
        if let Some(permissions) = permissions {
            // We need to set permissions after creating the file. The file might have been
            // just truncated and we want to enforce the new permissions. But even when the
            // file got created, the opts.mode got umasked, and we need to enforce the exact
            // permissions.
            f.set_permissions(permissions)?;
        }
        for line in data {
            f.write_all(line)?;
        }
        drop(f);
        if let Some(new_path) = &rename_to {
            fs::rename(path, new_path)?;
        }
        Ok(())
    }
}

impl SharedVirtualFilesystem {
    pub(crate) fn new(vfs: VirtualFilesystem) -> Self {
        Self(Arc::new(vfs))
    }
}

impl VirtualFilesystem {
    pub(crate) fn new(downstream_repo: Git) -> Self {
        Self {
            downstream_repo,
            virtual_files: HashMap::new(),
        }
    }

    fn insert_file(
        &mut self,
        path: &OsStr,
        new_path: Option<&OsStr>,
        content: Option<Vec<u8>>,
    ) {
        self.virtual_files.insert(
            path.as_bytes().to_vec(),
            VirtualFile {
                new_path: new_path.map(|path| path.as_bytes().to_vec()),
                content,
            },
        );
    }

    fn resolve_file<'a>(&'a self, mut name: &'a [u8], recursive: bool)
        -> Result<Option<TypedLines>, String>
    {
        while let Some(file) = self.virtual_files.get(name) {
            if let Some(new_path) = &file.new_path {
                // renamed file
                if !recursive {
                    return Ok(None);
                }
                name = new_path;
            } else if let Some(content) = &file.content {
                // modified file
                return Ok(Some(TypedLines::from_vec_and_name(content.to_vec(), name)));
            } else {
                // deleted file
                return Ok(None);
            }
        }
        match TypedLines::from_file(name) {
            Ok(lines) => Ok(Some(lines)),
            Err(err) => if err.kind() == io::ErrorKind::NotFound {
                Ok(None)
            } else {
                // any other error than NotFound is always fatal
                Err(format!("Cannot open file {}: {}", name.as_bstr(), err))
            }
        }
    }

    pub(crate) fn get_file(&self, name: &[u8]) -> Result<Option<TypedLines>, String> {
        self.resolve_file(name, false)
    }

    pub(crate) fn get_renamed_file(&self, old_path: &[u8], fork_point: Option<&[u8]>)
        -> Result<Option<TypedLines>, String>
    {
        if let Some(new_path) = self.downstream_repo
            .future_path(b"HEAD", fork_point, old_path)
            .map_err(|err| format!(
                "Error getting renames from the target repository: {err}"
            ))?
        {
            self.resolve_file(&new_path, true)
        } else {
            Ok(None)
        }
    }
}

impl FilesystemWrite for VirtualFilesystem {
    fn exists(&self, path: &OsStr) -> io::Result<bool> {
        if let Some(file) = self.virtual_files.get(path.as_bytes()) {
            Ok(file.content.is_some())
        } else {
            fs::exists(path)
        }
    }

    fn remove_file(&mut self, path: &OsStr) -> io::Result<()> {
        self.insert_file(path, None, None);
        Ok(())
    }

    fn create_file<'a>(
        &mut self,
        path: &OsStr,
        rename_to: Option<&OsStr>,
        _permissions: Option<fs::Permissions>,
        data: impl Iterator<Item = &'a [u8]>,
    ) -> io::Result<()> {
        let path = if let Some(new_path) = rename_to {
            self.insert_file(path, Some(new_path), None);
            new_path
        } else {
            path
        };
        let data = data.map(|line| line.iter()).flatten().copied().collect();
        self.insert_file(path, None, Some(data));
        Ok(())
    }
}
