use crate::{
    FileMetaData, FilePathMetaData, Integer, get_file_metadata, get_path_metadata,
    requirements::StringOrInclude,
};
use crate::{Result, guard};
use anyhow::Context;
use bon::Builder;
use serde::{Deserialize, Serialize};
use std::{fs, path::Path};
use url::Url;

#[derive(Serialize, Deserialize, Debug, Copy, PartialEq, Hash, Clone, Eq)]
#[serde(rename_all = "snake_case")]
pub enum LoadListingEnum {
    NoListing,
    ShallowListing,
    DeepListing,
}

#[derive(Serialize, Deserialize, Debug, PartialEq, Hash, Clone, Eq)]
#[serde(tag = "class")]
pub enum FileOrDirectory {
    File(File),
    Directory(Directory),
}

impl FileOrDirectory {
    #[must_use]
    pub fn path(&self) -> Option<&String> {
        match self {
            Self::File(f) => f.path.as_ref(),
            Self::Directory(d) => d.path.as_ref(),
        }
    }
    #[must_use]
    pub fn location(&self) -> Option<&String> {
        match self {
            Self::File(f) => f.location.as_ref(),
            Self::Directory(d) => d.location.as_ref(),
        }
    }

    pub fn set_path(&mut self, path: Option<String>) {
        match self {
            Self::File(f) => f.path = path,
            Self::Directory(d) => d.path = path,
        }
    }

    pub fn set_location(&mut self, location: Option<String>) {
        match self {
            Self::File(f) => f.location = location,
            Self::Directory(d) => d.location = location,
        }
    }

    #[must_use]
    pub fn basename(&self) -> Option<&String> {
        match self {
            Self::File(f) => f.basename.as_ref(),
            Self::Directory(d) => d.basename.as_ref(),
        }
    }

    #[must_use]
    pub fn is_file(&self) -> bool {
        matches!(self, FileOrDirectory::File(_))
    }

    #[must_use]
    pub fn is_dir(&self) -> bool {
        matches!(self, FileOrDirectory::Directory(_))
    }

    #[must_use]
    /// Gets a `FileOrDirectory` from its yaml Mapping
    /// # Panics
    /// if class is not given
    pub fn from_mapping(value: serde_json::Value) -> Self {
        serde_json::from_value(value).expect("class not found")
    }
}

#[doc = include_str!("docs/File.md")]
#[derive(Serialize, Deserialize, Debug, PartialEq, Hash, Clone, Default, Builder, Eq)]
#[serde(rename_all = "camelCase")]
/// - Reference: <https://www.commonwl.org/v1.2/CommandLineTool.html#File>
pub struct File {
    #[doc = include_str!("docs/File_location.md")]
    #[serde(skip_serializing_if = "Option::is_none")]
    #[builder(into)]
    pub location: Option<String>,
    #[doc = include_str!("docs/File_path.md")]
    #[serde(skip_serializing_if = "Option::is_none")]
    #[builder(into)]
    pub path: Option<String>,
    #[doc = include_str!("docs/File_basename.md")]
    #[serde(skip_serializing_if = "Option::is_none")]
    #[builder(into)]
    pub basename: Option<String>,
    #[doc = include_str!("docs/File_dirname.md")]
    #[serde(skip_serializing_if = "Option::is_none")]
    #[builder(into)]
    pub dirname: Option<String>,
    #[doc = include_str!("docs/File_nameroot.md")]
    #[serde(skip_serializing_if = "Option::is_none")]
    #[builder(into)]
    pub nameroot: Option<String>,
    #[doc = include_str!("docs/File_nameext.md")]
    #[serde(skip_serializing_if = "Option::is_none")]
    #[builder(into)]
    pub nameext: Option<String>,
    #[doc = include_str!("docs/File_checksum.md")]
    #[serde(skip_serializing_if = "Option::is_none")]
    #[builder(into)]
    pub checksum: Option<String>,
    #[doc = include_str!("docs/File_size.md")]
    #[serde(skip_serializing_if = "Option::is_none")]
    #[builder(into)]
    pub size: Option<Integer>,
    #[doc = include_str!("docs/File_secondaryFiles.md")]
    #[serde(skip_serializing_if = "Option::is_none")]
    #[builder(into)]
    pub secondary_files: Option<Vec<FileOrDirectory>>,
    #[doc = include_str!("docs/File_format.md")]
    #[serde(skip_serializing_if = "Option::is_none")]
    #[builder(into)]
    pub format: Option<String>,
    #[doc = include_str!("docs/File_contents.md")]
    #[serde(skip_serializing_if = "Option::is_none")]
    #[builder(into)]
    pub contents: Option<String>,
}

impl File {
    /// Creats a `File` from `Path`.
    /// # Errors
    /// if file does not exist
    pub fn new_from_path(path: &Path) -> Result<Self> {
        let path_as_str = path.to_string_lossy();
        let FilePathMetaData {
            basename,
            nameroot,
            nameext,
            dirname,
        } = get_path_metadata(path);

        let FileMetaData { size, checksum } = get_file_metadata(path)?;

        let file = File::builder()
            .location(
                Url::from_file_path(path)
                    .map_err(|()| anyhow::anyhow!("Could not create URL from {path_as_str}"))?
                    .to_string(),
            )
            .path(path_as_str)
            .maybe_basename(basename)
            .maybe_nameroot(nameroot)
            .maybe_nameext(nameext)
            .maybe_dirname(dirname)
            .size(size)
            .maybe_checksum(checksum)
            .build();
        Ok(file)
    }
}

#[doc = include_str!("docs/Directory.md")]
#[derive(Serialize, Deserialize, Debug, PartialEq, Hash, Clone, Default, Builder, Eq)]
#[serde(rename_all = "camelCase")]
/// - Reference: <https://www.commonwl.org/v1.2/CommandLineTool.html#Directory>
pub struct Directory {
    #[doc = include_str!("docs/Directory_location.md")]
    #[serde(skip_serializing_if = "Option::is_none")]
    #[builder(into)]
    pub location: Option<String>,
    #[doc = include_str!("docs/Directory_path.md")]
    #[serde(skip_serializing_if = "Option::is_none")]
    #[builder(into)]
    pub path: Option<String>,
    #[doc = include_str!("docs/Directory_basename.md")]
    #[serde(skip_serializing_if = "Option::is_none")]
    #[builder(into)]
    pub basename: Option<String>,
    #[doc = include_str!("docs/Directory_listing.md")]
    #[serde(skip_serializing_if = "Option::is_none")]
    #[builder(into)]
    pub listing: Option<Vec<FileOrDirectory>>,
}

impl Directory {
    /// Creates a directory from a given path
    /// # Errors
    /// If dir does not exist
    pub fn new_from_path(path: &Path) -> Result<Self> {
        let path_as_str = path.to_string_lossy();
        let FilePathMetaData {
            basename,
            nameroot: _,
            nameext: _,
            dirname: _,
        } = get_path_metadata(path);

        let dir = Directory::builder()
            .location(
                Url::from_file_path(path)
                    .map_err(|()| anyhow::anyhow!("Could not create URL from {path_as_str}"))?
                    .to_string(),
            )
            .path(path_as_str)
            .maybe_basename(basename)
            .build();
        Ok(dir)
    }

    /// Loads listing for directory
    /// # Errors
    /// if a file or dir does not exist or path is not given
    pub fn load_listing(&mut self, load_listing: LoadListingEnum) -> Result<()> {
        guard!(let Some(path) = &self.path, "No path given");

        match load_listing {
            LoadListingEnum::NoListing => self.listing = None,
            LoadListingEnum::ShallowListing => self.listing = Some(Self::read_dir(path, false)?),
            LoadListingEnum::DeepListing => self.listing = Some(Self::read_dir(path, true)?),
        }
        Ok(())
    }

    fn read_dir(path: &str, recursive: bool) -> Result<Vec<FileOrDirectory>> {
        let mut entries = Vec::new();

        let read_dir =
            fs::read_dir(path).with_context(|| format!("Could not read directory {path}"))?;

        for entry in read_dir.flatten() {
            let path_buf = entry.path();
            let name = entry.file_name().to_string_lossy().to_string();

            if path_buf.is_dir() {
                let mut dir = Directory {
                    path: Some(path_buf.to_string_lossy().to_string()),
                    basename: Some(name),
                    ..Default::default()
                };
                dir.location = dir
                    .path
                    .as_ref()
                    .map(|s| Url::from_file_path(s).unwrap().to_string());

                if recursive {
                    dir.load_listing(LoadListingEnum::DeepListing)?;
                }

                entries.push(FileOrDirectory::Directory(dir));
            } else {
                entries.push(FileOrDirectory::File(File::new_from_path(&path_buf)?));
            }
        }
        entries.sort_by_key(|e| e.basename().cloned());

        Ok(entries)
    }
}

#[doc = include_str!("docs/Dirent.md")]
#[derive(Serialize, Deserialize, Debug, PartialEq, Hash, Clone, Builder)]
#[serde(rename_all = "camelCase")]
/// - Reference: <https://www.commonwl.org/v1.2/CommandLineTool.html#Dirent>
pub struct Dirent {
    #[doc = include_str!("docs/Dirent_entry.md")]
    #[builder(into)]
    pub entry: StringOrInclude,
    #[doc = include_str!("docs/Dirent_entryname.md")]
    #[serde(skip_serializing_if = "Option::is_none")]
    #[builder(into)]
    pub entryname: Option<String>,
    #[doc = include_str!("docs/Dirent_writable.md")]
    #[serde(skip_serializing_if = "Option::is_none")]
    #[builder(into)]
    pub writable: Option<bool>,
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    #[allow(unused)]
    pub fn test_file_or_directory() {
        #[derive(Deserialize)]
        struct ListingBag {
            listing: Vec<FileOrDirectory>,
        }
        let contents = include_str!("../../testdata/listing.yaml");
        let res = serde_saphyr::from_str::<ListingBag>(contents);
        assert!(res.is_ok());
    }
}
