use crate::errors::ToeErrorType;
use crate::thumbsize::ThumbSize;

use log::{error, info, trace};
use percent_encoding::{AsciiSet, CONTROLS};
use std::env;
use std::ffi::OsStr;
use std::fs::{canonicalize, set_permissions, File, Permissions};
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};

fn calc_file_uri(use_full_path_for_md5: bool, path: &std::path::Path) -> String {
    //! For a given file, this method returns the URI which is used for the thumbnail's MD5.
    //! Taken and modified from "thumbnailer".
    //!
    //! Currently, this works only with absolute paths.
    //! That's a precondition not properly error-handled in here.
    const FRAGMENT: &AsciiSet = &CONTROLS.add(b' ').add(b'"').add(b'<').add(b'>').add(b'`');
    const PATH_SET: &AsciiSet = &FRAGMENT.add(b'#').add(b'?').add(b'{').add(b'}');
    const USER_INFO_SET: &AsciiSet = &PATH_SET
        .add(b'/')
        .add(b':')
        .add(b';')
        .add(b'=')
        .add(b'@')
        .add(b'[')
        .add(b'\\')
        .add(b']')
        .add(b'^')
        .add(b'|');

    assert!(path.is_absolute());

    if use_full_path_for_md5 {
        let mut encoded = String::new();

        for t in path.iter() {
            if t == OsStr::new(&std::path::MAIN_SEPARATOR.to_string()) {
                continue;
            } else {
                encoded.push(std::path::MAIN_SEPARATOR);
                encoded +=
                    &percent_encoding::utf8_percent_encode(t.to_str().unwrap(), USER_INFO_SET)
                        .to_string();
            }
        }
        format!("file://{encoded}")
    } else {
        percent_encoding::utf8_percent_encode(
            path.file_name().unwrap().to_str().unwrap(),
            USER_INFO_SET,
        )
        .to_string()
    }
}

fn calc_thumb_file_name(uri: &String) -> String {
    let vec = md5::compute(uri).to_vec();
    format!("{}.png", hex::encode(vec))
}

fn get_thumb_dir() -> Result<PathBuf, ToeErrorType> {
    let cache_dir: PathBuf = match env::var("XDG_CACHE_HOME") {
        Ok(v) => v.as_str().into(),
        Err(_) => match env::var("HOME") {
            Ok(v) => [v.as_str(), ".cache"].iter().collect(),
            Err(_) => return Err(ToeErrorType::NoCacheIdentifiable),
        },
    };

    match std::path::Path::new(&cache_dir).exists() {
        true => {
            let canon_cache_dir = canonicalize(&cache_dir).map_err(|e| {
                ToeErrorType::FileAllocationError(format!(
                    "Path to thumb repository {:?} cannot be canonicalized: {}",
                    &cache_dir, e
                ))
            })?;
            let base_dir: PathBuf = [canon_cache_dir, "thumbnails".into()].iter().collect();
            match std::path::Path::new(&base_dir).exists() {
                true => Ok(base_dir),
                false => {
                    info!(
                        "Thumbnail base directory {} does not exist. Creating it now.",
                        base_dir.display()
                    );
                    match std::fs::create_dir(base_dir.clone()) {
                        Ok(_) => {
                            match set_permissions(base_dir.clone(), Permissions::from_mode(0o700)) {
                                Err(error) => Err(ToeErrorType::CanNotSetPermission(
                                    base_dir.into_os_string().into_string().unwrap(),
                                    error.to_string(),
                                )),
                                Ok(_) => Ok(base_dir),
                            }
                        }
                        Err(error) => {
                            error!("Directory creation error: {:?}", error);
                            Err(ToeErrorType::ThumbDirCantBeCreated(format!(
                                "{}",
                                base_dir.display()
                            )))
                        }
                    }
                }
            }
        }
        false => Err(ToeErrorType::CacheDoesNotExist(match cache_dir.to_str() {
            Some(v) => String::from(v),
            None => String::from(""),
        })),
    }
}

#[derive(Debug)]
pub struct ThumbRequest {
    pub size: ThumbSize,

    // The full, canonicalized (symlink-free, absolut and direct) path to the input file.
    pub file_path: String,
    pub file_uri: String,
    pub thumb_file_name: String,

    // The directory used as thumb repositiory.
    // The size specific directoris are direct children of this directory.
    pub thumb_dir: PathBuf,
    pub process_id: u32,
    pub mtime_secs: u64,
    pub file_size: u64,
    pub force_creation: bool,
}

/// A thumbnail, instantiated just by the path to the file for which the thumbnail is requested
/// and the thumbnails size.
impl ThumbRequest {
    pub fn new(
        file_path: String,
        size: ThumbSize,
        force_creation: bool,
    ) -> Result<ThumbRequest, ToeErrorType> {
        let path = std::path::Path::new(&file_path);
        match path.exists() {
            true => match canonicalize(path) {
                Ok(canon_path) => {
                    let file_uri = calc_file_uri(true, &canon_path);
                    let thumb_file_name = calc_thumb_file_name(&file_uri);
                    let thumb_dir = get_thumb_dir()?;
                    let metadata = std::fs::metadata(&canon_path).unwrap();
                    Ok(ThumbRequest {
                        file_path,
                        size,
                        file_uri,
                        thumb_file_name,
                        thumb_dir,
                        process_id: std::process::id(),
                        mtime_secs: metadata
                            .modified()
                            .unwrap()
                            .duration_since(std::time::UNIX_EPOCH)
                            .unwrap()
                            .as_secs(),
                        file_size: metadata.len(),
                        force_creation,
                    })
                }
                Err(_) => Err(ToeErrorType::FileAllocationError(String::from(
                    "Path cannot be canonicalized.",
                ))),
            },
            false => Err(ToeErrorType::FileDoesNotExist),
        }
    }

    /// For a given thumb size, returns the base directory where to store a
    /// thumb.
    ///
    /// The directory is created in case it does not exist.
    pub fn get_toe_path(&self, size: ThumbSize) -> Result<PathBuf, ToeErrorType> {
        let mut toe_path = self.thumb_dir.to_path_buf();
        toe_path.push(size.dir_name());
        let path = toe_path.as_path();
        if !path.exists() {
            trace!("Path {} does not exist. Creating it now.", path.display());
            let creation_result = std::fs::create_dir_all(path);
            if creation_result.is_err() {
                error!("Directory creation error: {:?}", creation_result);
                return Err(ToeErrorType::ThumbDirCantBeCreated(format!(
                    "{}",
                    path.display()
                )));
            }
            if let Err(error) = set_permissions(path, Permissions::from_mode(0o700)) {
                return Err(ToeErrorType::CanNotSetPermission(
                    Path::new(path).to_str().unwrap().to_string(),
                    error.to_string(),
                ));
            }
        }
        toe_path.push(&self.thumb_file_name);
        Ok(toe_path)
    }

    /// Check if a valid thumbnail with the given size exist.
    /// A valid thumbnail must also have the exact same time-stamp as the original.
    ///
    /// If yes, returns `Some` path to that thumbnail, otherwise returns `None`.
    ///
    pub fn get_path_to_thumb_if_existing_and_valid(
        &self,
        size: ThumbSize,
    ) -> Result<Option<PathBuf>, ToeErrorType> {
        let toe_path = self.get_toe_path(size)?;
        if toe_path.as_path().exists() {
            let decoder = png::Decoder::new(File::open(&toe_path).unwrap());
            let reader = decoder.read_info().unwrap();
            let mtime_secs = &reader
                .info()
                .uncompressed_latin1_text
                .iter()
                .find(|c| c.keyword == "Thumb::MTime");
            if let Some(mtime_secs) = mtime_secs {
                let mtime_secs = mtime_secs.text.parse::<u64>();
                if let Ok(mtime_secs) = mtime_secs {
                    if mtime_secs == self.mtime_secs {
                        trace!("Check availability: Thumb file exists.");
                        Ok(Some(toe_path))
                    } else {
                        trace!(
                            "Check availability: Thumb file exists but has deviating mtime value."
                        );
                        Ok(None)
                    }
                } else {
                    trace!(
                        "Check availability: Thumb file exists but mtime value is no valid u64."
                    );
                    Ok(None)
                }
            } else {
                trace!("Check availability: Thumb file exists but has no mtime value.");
                Ok(None)
            }
        } else {
            trace!("Check availability: Thumb file does not exist.");
            Ok(None)
        }
    }
}
