use upend::database::stores::UpStore;
use upend::util::hash::b58_encode;
use upend::util::hash::Hash;
use upend::util::jobs::{JobContainer, JobState};
use anyhow::{anyhow, Result};
use tracing::{debug, trace};

use std::{
    collections::HashMap,
    fs::File,
    io::Write,
    path::{Path, PathBuf},
    sync::{Arc, Mutex},
};

use self::audio::AudioPath;
use self::image::ImagePath;
use self::text::TextPath;
use self::video::VideoPath;

pub mod audio;
pub mod image;
pub mod text;
pub mod video;

pub trait Previewable {
    fn get_thumbnail(&self, options: HashMap<String, String>) -> Result<Option<Vec<u8>>>;
}

type HashWithOptions = (Hash, String);
pub struct PreviewStore {
    path: PathBuf,
    store: Arc<Box<dyn UpStore + Send + Sync>>,

    locks: Mutex<HashMap<HashWithOptions, Arc<Mutex<PathBuf>>>>,
}

#[cfg(feature = "previews")]
impl PreviewStore {
    pub fn new<P: AsRef<Path>>(path: P, store: Arc<Box<dyn UpStore + Send + Sync>>) -> Self {
        PreviewStore {
            path: PathBuf::from(path.as_ref()),
            store,
            locks: Mutex::new(HashMap::new()),
        }
    }

    fn get_path(&self, hash: &Hash, options: &HashMap<String, String>) -> Arc<Mutex<PathBuf>> {
        let mut locks = self.locks.lock().unwrap();
        let mut options_strs = options
            .iter()
            .map(|(k, v)| format!("{k}{v}"))
            .collect::<Vec<String>>();
        options_strs.sort();
        let options_concat = options_strs.concat();
        if let Some(path) = locks.get(&(hash.clone(), options_concat.clone())) {
            path.clone()
        } else {
            let thumbpath = self.path.join(format!(
                "{}{}",
                b58_encode(hash),
                if options_concat.is_empty() {
                    String::from("")
                } else {
                    format!("_{options_concat}")
                }
            ));

            let path = Arc::new(Mutex::new(thumbpath));
            locks.insert((hash.clone(), options_concat), path.clone());
            path
        }
    }

    pub fn get(
        &self,
        hash: Hash,
        options: HashMap<String, String>,
        mut job_container: JobContainer,
    ) -> Result<Option<PathBuf>> {
        debug!("Preview for {hash:?} requested...");
        let path_mutex = self.get_path(&hash, &options);
        let thumbpath = path_mutex.lock().unwrap();
        if thumbpath.exists() {
            trace!("Preview for {hash:?} already exists, returning {thumbpath:?}");
            Ok(Some(thumbpath.clone()))
        } else {
            trace!("Calculating preview for {hash:?}...");
            let files = self.store.retrieve(&hash)?;
            if let Some(file) = files.get(0) {
                let file_path = file.get_file_path();
                let mut job_handle = job_container.add_job(
                    None,
                    &format!("Creating preview for {:?}", file_path.file_name().unwrap()),
                )?;

                let mime_type = options.get("mime").map(|x| x.to_owned());

                let mime_type: Option<String> = if mime_type.is_some() {
                    mime_type
                } else {
                    tree_magic_mini::from_filepath(file_path).map(|m| m.into())
                };

                let preview = match mime_type {
                    Some(tm) if tm.starts_with("text") => {
                        TextPath(file_path).get_thumbnail(options)
                    }
                    Some(tm) if tm.starts_with("video") || tm == "application/x-matroska" => {
                        VideoPath(file_path).get_thumbnail(options)
                    }
                    Some(tm) if tm.starts_with("audio") || tm == "application/x-riff" => {
                        AudioPath(file_path).get_thumbnail(options)
                    }
                    Some(tm) if tm.starts_with("image") => {
                        ImagePath(file_path).get_thumbnail(options)
                    }
                    Some(unknown) => Err(anyhow!("No capability for {:?} thumbnails.", unknown)),
                    _ => Err(anyhow!("Unknown file type, or file doesn't exist.")),
                };

                match preview {
                    Ok(preview) => {
                        trace!("Got preview for {hash:?}.");

                        let _ = job_handle.update_state(JobState::Done);

                        if let Some(data) = preview {
                            std::fs::create_dir_all(&self.path)?;
                            let mut file = File::create(&*thumbpath)?;
                            file.write_all(&data)?;
                            Ok(Some(thumbpath.clone()))
                        } else {
                            Ok(None)
                        }
                    }
                    Err(err) => Err(err),
                }
            } else {
                Err(anyhow!("Object not found, or is not a file."))
            }
        }
    }
}
