Documentation
use file_paths::FlexPath;

use crate::common::*;
use std::{io, path::PathBuf};

#[derive(Eq, PartialEq, Copy, Clone)]
enum EntryKind {
    File,
    Directory,
}

#[derive(Clone)]
pub struct FileSystemReference(pub PathBuf, EntryKind);

impl FileSystemReference {
    pub fn name(&self) -> String {
        FlexPath::new_native(&self.0.to_string_lossy().into_owned()).base_name()
    }

    pub fn as_directory(&self) -> Option<DirectoryReference> {
        if self.1 == EntryKind::Directory { Some(DirectoryReference(self.0.clone())) } else { None }
    }

    pub fn as_file(&self) -> Option<FileReference> {
        if self.1 == EntryKind::File { Some(FileReference(self.0.clone())) } else { None }
    }
}

#[derive(Clone)]
pub struct FileReference(pub PathBuf);

impl FileReference {
    pub async fn read_bytes(&self) -> io::Result<Bytes> {
        let bytes = tokio::fs::read(&self.0).await?;
        Ok(Bytes::from(bytes))
    }

    pub async fn read_utf8(&self) -> io::Result<String> {
        tokio::fs::read_to_string(&self.0).await
    }

    pub async fn write(&self, data: &[u8]) -> io::Result<()> {
        tokio::fs::write(&self.0, data).await
    }

    pub async fn modification_date(&self) -> io::Result<std::time::SystemTime> {
        tokio::fs::metadata(&self.0).await.and_then(|metadata| metadata.modified())
    }

    pub fn name(&self) -> String {
        FlexPath::new_native(&self.0.to_string_lossy().into_owned()).base_name()
    }

    pub async fn size(&self) -> io::Result<usize> {
        tokio::fs::metadata(&self.0).await.map(|metadata| metadata.len() as usize)
    }
}

impl From<FileReference> for FileSystemReference {
    fn from(value: FileReference) -> Self {
        FileSystemReference(value.0.clone(), EntryKind::File)
    }
}

#[derive(Clone)]
pub struct DirectoryReference(pub PathBuf);

impl DirectoryReference {
    pub fn name(&self) -> String {
        FlexPath::new_native(&self.0.to_string_lossy().into_owned()).base_name()
    }

    pub async fn entries(&self) -> io::Result<Vec<FileSystemReference>> {
        let mut listing_1 = tokio::fs::read_dir(&self.0).await?;
        let mut listing_2 = vec![];
        loop {
            let entry = listing_1.next_entry().await;
            if entry.is_err() {
                continue;
            }
            let entry = entry.unwrap();
            if entry.is_none() {
                break;
            }
            let entry = entry.unwrap();
            let reference = FileSystemReference(entry.path(), if entry.file_type().await?.is_dir() { EntryKind::Directory } else { EntryKind::File });
            listing_2.push(reference);
        }
        Ok(listing_2)
    }

    pub async fn get_directory(&self, name: &str) -> io::Result<DirectoryReference> {
        if !is_valid_name(name) {
            return Err(io::Error::new(io::ErrorKind::InvalidFilename, "Invalid filename"));
        }
        let path = self.0.join(name);
        let metadata = tokio::fs::metadata(&path).await?;
        if metadata.is_dir() {
            Ok(DirectoryReference(path))
        } else {
            Err(io::Error::new(io::ErrorKind::NotADirectory, "Not a directory"))
        }
    }

    pub async fn get_directory_or_create(&self, name: &str) -> io::Result<DirectoryReference> {
        if !is_valid_name(name) {
            return Err(io::Error::new(io::ErrorKind::InvalidFilename, "Invalid filename"));
        }
        let path = self.0.join(name);

        let metadata = tokio::fs::metadata(&path).await;
        if let Err(error) = metadata {
            if error.kind() == io::ErrorKind::NotFound {
                tokio::fs::create_dir(&path).await?;
                return Ok(DirectoryReference(path));
            }
            return Err(error);
        }
        let metadata = metadata.unwrap();

        if metadata.is_dir() {
            Ok(DirectoryReference(path))
        } else {
            Err(io::Error::new(io::ErrorKind::NotADirectory, "Not a directory"))
        }
    }

    pub async fn get_file(&self, name: &str) -> io::Result<FileReference> {
        if !is_valid_name(name) {
            return Err(io::Error::new(io::ErrorKind::InvalidFilename, "Invalid filename"));
        }
        let path = self.0.join(name);
        let metadata = tokio::fs::metadata(&path).await?;
        if metadata.is_file() {
            Ok(FileReference(path))
        } else {
            Err(io::Error::new(io::ErrorKind::IsADirectory, "Entry is a directory"))
        }
    }

    pub async fn get_file_or_create(&self, name: &str) -> io::Result<FileReference> {
        if !is_valid_name(name) {
            return Err(io::Error::new(io::ErrorKind::InvalidFilename, "Invalid filename"));
        }
        let path = self.0.join(name);

        let metadata = tokio::fs::metadata(&path).await;
        if let Err(error) = metadata {
            if error.kind() == io::ErrorKind::NotFound {
                tokio::fs::write(&path, vec![]).await?;
                return Ok(FileReference(path));
            }
            return Err(error);
        }
        let metadata = metadata.unwrap();

        if metadata.is_file() {
            Ok(FileReference(path))
        } else {
            Err(io::Error::new(io::ErrorKind::IsADirectory, "Entry is a directory"))
        }
    }

    pub async fn delete_empty_directory(&self, name: &str) -> io::Result<()> {
        if !is_valid_name(name) {
            return Err(io::Error::new(io::ErrorKind::InvalidFilename, "Invalid filename"));
        }
        tokio::fs::remove_dir(self.0.join(name)).await
    }

    pub async fn delete_directory_all(&self, name: &str) -> io::Result<()> {
        if !is_valid_name(name) {
            return Err(io::Error::new(io::ErrorKind::InvalidFilename, "Invalid filename"));
        }
        tokio::fs::remove_dir_all(self.0.join(name)).await
    }

    pub async fn delete_file(&self, name: &str) -> io::Result<()> {
        if !is_valid_name(name) {
            return Err(io::Error::new(io::ErrorKind::InvalidFilename, "Invalid filename"));
        }
        tokio::fs::remove_file(self.0.join(name)).await
    }
}

impl From<DirectoryReference> for FileSystemReference {
    fn from(value: DirectoryReference) -> Self {
        FileSystemReference(value.0.clone(), EntryKind::Directory)
    }
}

fn is_valid_name(name: &str) -> bool {
    let path = std::path::PathBuf::from(name);
    let name_2 = path.file_name();
    if let Some(name_2) = name_2 { name == name_2 } else { false }
}