use super::{fileinfo::FileInfo, FileId, ReadFileError, WriteFileError};
use chrono::prelude::*;
use hex_string::HexString;
use image::imageops::FilterType;
use sha2::{Digest, Sha256};
use std::{
    convert::TryFrom,
    io::{Read, Write},
    path::{Path, PathBuf},
};
use thiserror::Error;
use uuid::Uuid;

#[derive(Debug, Error)]
pub enum PathError {
    #[error("path cannot be derived from input")]
    InvalidPath,
}

#[derive(Clone, Debug)]
pub struct PathResolver {
    base: PathBuf,
    id: FileId,
    extension: String,
}

impl PathResolver {
    pub fn new(base: &Path, id: FileId, extension: String) -> Self {
        Self {
            base: base.to_owned(),
            id,
            extension,
        }
    }

    pub fn metadata_path_by_id(base: &Path, id: FileId) -> PathBuf {
        let mut path = base.to_path_buf();
        path.push(PathBuf::from(id.clone()));
        path.set_extension("json");
        path
    }

    pub fn id(&self) -> FileId {
        self.id.clone()
    }

    pub fn file_path(&self) -> PathBuf {
        let mut path = self.base.clone();
        path.push(PathBuf::from(self.id.clone()));
        path.set_extension(self.extension.clone());
        path
    }

    pub fn metadata_path(&self) -> PathBuf {
        let mut path = self.base.clone();
        path.push(PathBuf::from(self.id.clone()));
        path.set_extension("json");
        path
    }

    pub fn thumbnail_path(&self) -> PathBuf {
        let mut path = self.base.clone();
        path.push(PathBuf::from(self.id.clone()));
        path.set_extension(format!("tn.{}", self.extension));
        path
    }
}

impl TryFrom<String> for PathResolver {
    type Error = PathError;
    fn try_from(s: String) -> Result<Self, Self::Error> {
        PathResolver::try_from(s.as_str())
    }
}

impl TryFrom<&str> for PathResolver {
    type Error = PathError;
    fn try_from(s: &str) -> Result<Self, Self::Error> {
        PathResolver::try_from(Path::new(s))
    }
}

impl TryFrom<PathBuf> for PathResolver {
    type Error = PathError;
    fn try_from(path: PathBuf) -> Result<Self, Self::Error> {
        PathResolver::try_from(path.as_path())
    }
}

impl TryFrom<&Path> for PathResolver {
    type Error = PathError;
    fn try_from(path: &Path) -> Result<Self, Self::Error> {
        Ok(Self {
            base: path
                .parent()
                .map(|s| s.to_owned())
                .ok_or(PathError::InvalidPath)?,
            id: path
                .file_stem()
                .and_then(|s| s.to_str().map(FileId::from))
                .ok_or(PathError::InvalidPath)?,
            extension: path
                .extension()
                .and_then(|s| s.to_str().map(|s| s.to_owned()))
                .ok_or(PathError::InvalidPath)?,
        })
    }
}

/// One file in the database, complete with the path of the file and information about the
/// thumbnail of the file.
#[derive(Debug)]
pub struct FileHandle {
    pub id: FileId,
    pub path: PathResolver,
    pub info: FileInfo,
}

impl FileHandle {
    /// Create a new entry in the database
    pub fn new(filename: String, root: PathBuf) -> Result<Self, WriteFileError> {
        let id = FileId::from(Uuid::new_v4().hyphenated().to_string());

        let extension = PathBuf::from(filename)
            .extension()
            .and_then(|s| s.to_str().map(|s| s.to_owned()))
            .ok_or(WriteFileError::InvalidPath)?;
        let path = PathResolver {
            base: root.clone(),
            id: id.clone(),
            extension: extension.clone(),
        };

        let file_type = mime_guess::from_ext(&extension)
            .first_or_text_plain()
            .essence_str()
            .to_owned();

        let info = FileInfo {
            id: id.clone(),
            size: 0,
            created: Utc::now(),
            file_type,
            hash: "".to_owned(),
            extension,
        };

        let mut md_file = std::fs::File::create(path.metadata_path())?;
        let _ = md_file.write(&serde_json::to_vec(&info)?)?;

        Ok(Self { id, path, info })
    }

    pub fn load(id: &FileId, root: &Path) -> Result<Self, ReadFileError> {
        let info = FileInfo::load(PathResolver::metadata_path_by_id(root, id.clone()))?;
        let resolver = PathResolver::new(root, id.clone(), info.extension.clone());
        Ok(Self {
            id: info.id.clone(),
            path: resolver,
            info,
        })
    }

    pub fn set_content(&mut self, content: Vec<u8>) -> Result<(), WriteFileError> {
        let mut content_file = std::fs::File::create(self.path.file_path())?;
        let byte_count = content_file.write(&content)?;
        self.info.size = byte_count;
        self.info.hash = self.hash_content(&content).as_string();

        let mut md_file = std::fs::File::create(self.path.metadata_path())?;
        let _ = md_file.write(&serde_json::to_vec(&self.info)?)?;

        self.write_thumbnail()?;

        Ok(())
    }

    pub fn content(&self) -> Result<Vec<u8>, ReadFileError> {
        load_content(&self.path.file_path())
    }

    pub fn thumbnail(&self) -> Result<Vec<u8>, ReadFileError> {
        load_content(&self.path.thumbnail_path())
    }

    fn hash_content(&self, data: &Vec<u8>) -> HexString {
        HexString::from_bytes(&Sha256::digest(data).to_vec())
    }

    fn write_thumbnail(&self) -> Result<(), WriteFileError> {
        let img = image::open(self.path.file_path())?;
        let tn = img.resize(640, 640, FilterType::Nearest);
        tn.save(self.path.thumbnail_path())?;
        Ok(())
    }

    pub fn delete(self) {
        let _ = std::fs::remove_file(self.path.thumbnail_path());
        let _ = std::fs::remove_file(self.path.file_path());
        let _ = std::fs::remove_file(self.path.metadata_path());
    }
}

fn load_content(path: &Path) -> Result<Vec<u8>, ReadFileError> {
    let mut buf = Vec::new();
    let mut file = std::fs::File::open(path)?;
    file.read_to_end(&mut buf)?;
    Ok(buf)
}

#[cfg(test)]
mod test {
    use super::*;
    use std::{convert::TryFrom, path::PathBuf};
    use tempdir::TempDir;

    #[test]
    fn paths() {
        let resolver = PathResolver::try_from("path/82420255-d3c8-4d90-a582-f94be588c70c.png")
            .expect("to have a valid path");

        assert_eq!(
            resolver.file_path(),
            PathBuf::from("path/82420255-d3c8-4d90-a582-f94be588c70c.png")
        );
        assert_eq!(
            resolver.metadata_path(),
            PathBuf::from("path/82420255-d3c8-4d90-a582-f94be588c70c.json")
        );
        assert_eq!(
            resolver.thumbnail_path(),
            PathBuf::from("path/82420255-d3c8-4d90-a582-f94be588c70c.tn.png")
        );
    }

    #[test]
    fn it_opens_a_file() {
        let tmp = TempDir::new("var").unwrap();
        FileHandle::new("rawr.png".to_owned(), PathBuf::from(tmp.path())).expect("to succeed");
    }

    #[test]
    fn it_deletes_a_file() {
        let tmp = TempDir::new("var").unwrap();
        let f =
            FileHandle::new("rawr.png".to_owned(), PathBuf::from(tmp.path())).expect("to succeed");
        f.delete();
    }

    #[test]
    fn it_can_return_a_thumbnail() {
        let tmp = TempDir::new("var").unwrap();
        let _ =
            FileHandle::new("rawr.png".to_owned(), PathBuf::from(tmp.path())).expect("to succeed");
        /*
        assert_eq!(
            f.thumbnail(),
            Thumbnail {
                id: String::from("rawr.png"),
                root: PathBuf::from("var/"),
            },
        );
        */
    }

    #[test]
    fn it_can_return_a_file_stream() {
        let tmp = TempDir::new("var").unwrap();
        let _ =
            FileHandle::new("rawr.png".to_owned(), PathBuf::from(tmp.path())).expect("to succeed");
        // f.stream().expect("to succeed");
    }

    #[test]
    fn it_raises_an_error_when_file_not_found() {
        let tmp = TempDir::new("var").unwrap();
        match FileHandle::load(&FileId::from("rawr"), tmp.path()) {
            Err(ReadFileError::FileNotFound(_)) => assert!(true),
            _ => assert!(false),
        }
    }
}