use crate::app::AppState;
use std::path::{Path, PathBuf};

#[derive(Clone, Debug)]
pub struct StaticFilesManager {
    pub dir: PathBuf,
    pub index: Vec<String>,
}

const STATIC_ASSETS_DIRECTORIES: [&'static str; 6] = [
    "images",
    "sounds",
    "videos",
    "docs",
    "source_code",
    "default",
];

impl StaticFilesManager {
    pub fn new(app_state: &AppState) -> Result<Self, String> {
        match Self::create_dir_if_missing(&app_state.config.storage_dir) {
            Ok(dir) => Ok(StaticFilesManager {
                index: Vec::new(),
                dir,
            }),
            Err(msg) => Err(msg),
        }
    }

    #[cfg(test)]
    pub fn testing_new(test_dir: &PathBuf) -> Result<Self, String> {
        match Self::create_dir_if_missing(test_dir) {
            Ok(dir) => Ok(StaticFilesManager {
                index: Vec::new(),
                dir,
            }),
            Err(msg) => Err(msg),
        }
    }

    fn create_dir_if_missing(app_dir: &PathBuf) -> Result<PathBuf, String> {
        let static_dir = app_dir.join("static");

        if !static_dir.exists() {
            match std::fs::create_dir_all(&static_dir) {
                Ok(_) => {
                    if let Err(err) = Self::create_assets_directories_structure(&static_dir) {
                        return Err(format!("{}", err));
                    };

                    if let Err(err) = Self::copy_default_files(&static_dir) {
                        return Err(format!("{}", err));
                    }
                }
                Err(err) => return Err(format!("{}", err)),
            }
        }

        Ok(static_dir)
    }

    fn create_assets_directories_structure(root: &PathBuf) -> Result<(), std::io::Error> {
        for d in STATIC_ASSETS_DIRECTORIES {
            std::fs::create_dir(root.join(d))?;
        }

        Ok(())
    }

    fn copy_default_files(static_dir: &PathBuf) -> Result<(), String> {
        let local_default_static = std::env::current_dir().unwrap().join("default_static");
        let default_static = static_dir.join("default");
        let mut cpy_options = fs_extra::dir::CopyOptions::new();
        cpy_options.content_only = true;
        match fs_extra::dir::copy(local_default_static, default_static, &cpy_options) {
            Err(err) => Err(format!("{}", err)),
            Ok(_) => Ok(()),
        }
    }

    fn rec_read_dir(&mut self, root: &Path, strip_from: &Path) {
        for entry in root.read_dir().unwrap() {
            if let Ok(entry) = entry {
                if entry.path().is_dir() {
                    self.rec_read_dir(&entry.path(), strip_from);
                } else {
                    self.push_path(&entry.path().strip_prefix(strip_from).unwrap());
                }
            }
        }
    }

    fn validate_path(&self, path: &Path) -> bool {
        self.dir.join(self.clean_relative_path(path)).exists()
    }

    fn clean_relative_path(&self, path: &Path) -> PathBuf {
        match path.starts_with("/") {
            true => path.strip_prefix("/").unwrap().to_path_buf(),
            false => path.to_path_buf(),
        }
    }

    fn push_path(&mut self, path: &Path) -> Vec<PathBuf> {
        if self.validate_path(path) {
            self.index
                .push(format!("/{}", path.to_str().unwrap().to_owned()));
            vec![path.to_path_buf()]
        } else {
            println!(
                "[WARNING] Error building static file index. The file {:?} doesn't exist and will be removed from the index.",
                self.dir.join(self.clean_relative_path(path)),
            );
            Vec::new()
        }
    }

    pub fn add_pathes(&mut self, pathes: &Vec<String>) -> Vec<PathBuf> {
        let mut added: Vec<PathBuf> = Vec::new();
        for pth in pathes.iter() {
            let p = self.push_path(Path::new(pth));
            added.extend(p.iter().map(|pb| pb.clone()));
        }
        added
    }

    pub fn build(&mut self) -> Self {
        self.index = Vec::new();
        self.rec_read_dir(&self.dir.clone(), &self.dir.clone());
        self.clone()
    }
}

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

    fn create_test_dir() -> PathBuf {
        let pth = PathBuf::from("./test");
        let _ = std::fs::create_dir(&pth);
        pth
    }

    fn remove_test_dir(pth: &PathBuf) {
        let _ = std::fs::remove_dir_all(pth);
    }

    #[test]
    fn test_directory_structure() {
        let test_dir = create_test_dir();
        let _manager = StaticFilesManager::testing_new(&test_dir).unwrap();

        for d in STATIC_ASSETS_DIRECTORIES {
            let p = test_dir.join("static").join(d);
            let exists = p.exists();
            assert!(
                exists,
                "{} doesn't exist\n{:?}",
                p.display(),
                remove_test_dir(&test_dir)
            );
        }

        remove_test_dir(&test_dir);
    }

    #[test]
    fn test_indexation() {
        let test_dir = create_test_dir();
        let mut manager = StaticFilesManager::testing_new(&test_dir).unwrap();
        let file_pth = test_dir.join("static").join("docs").join("testing.txt");
        std::fs::File::create(&file_pth).unwrap();
        manager = manager.build();

        assert!(
            manager.index.contains(&"/docs/testing.txt".to_string()),
            "Index doesn't contain path /docs/testing.txt\n{:?}",
            remove_test_dir(&test_dir)
        );

        remove_test_dir(&test_dir);
    }

    #[test]
    fn test_push_path() {
        let test_dir = create_test_dir();
        let mut manager = StaticFilesManager::testing_new(&test_dir).unwrap().build();
        let file_pth = test_dir.join("static").join("docs").join("testing.txt");
        std::fs::File::create(&file_pth).unwrap();
        let indexed_path = Path::new("docs/testing.txt");
        let added = manager.push_path(&indexed_path);

        assert_eq!(
            added,
            vec![PathBuf::from("docs/testing.txt")],
            "Path was not added\n{:?}",
            remove_test_dir(&test_dir)
        );

        assert!(
            manager.index.contains(&"/docs/testing.txt".to_string()),
            "Index doesn't contain path /docs/testing.txt\n{:?}",
            remove_test_dir(&test_dir)
        );

        remove_test_dir(&test_dir);
    }

    #[test]
    fn test_push_unexisting_path() {
        let test_dir = create_test_dir();
        let mut manager = StaticFilesManager::testing_new(&test_dir).unwrap().build();
        let indexed_path = Path::new("images/unexisting.png");
        let added = manager.push_path(&indexed_path);

        assert_eq!(
            added.len(),
            0,
            "No path should have been added\n{:?}",
            remove_test_dir(&test_dir)
        );

        assert!(
            !manager
                .index
                .contains(&"/images/unexisting.png".to_string()),
            "Index shouldn't container unexisting path\n{:?}",
            remove_test_dir(&test_dir)
        );

        remove_test_dir(&test_dir);
    }
}