use crate::model::*;

use std::collections::*;
use std::io;
use std::path::{Path, PathBuf};

mod image_convert;
mod metadata_file;

pub type TextureFileResult = Result<Vec<u8>, TextureFileError>;
pub enum TextureFileError {
    NotFound,
    IoError(io::Error),
    ImageError(::image::ImageError),
}

impl From<io::Error> for TextureFileError {
    fn from(err: io::Error) -> Self {
        TextureFileError::IoError(err)
    }
}

impl From<::image::ImageError> for TextureFileError {
    fn from(err: ::image::ImageError) -> Self {
        TextureFileError::ImageError(err)
    }
}

pub struct DataStore {
    //data_path: PathBuf,
    base_dir: PathBuf,

    textures: HashSet<Texture>,

    id_index: HashMap<String, Texture>,
    name_index: HashMap<String, Texture>,

    _preview_cache: HashMap<(TextureFormat, Sha256), Vec<u8>>,
}

impl DataStore {
    fn texture_base_path(&self) -> PathBuf {
        let mut path = self.base_dir.clone();
        path.push("textures");
        path
    }

    fn texture_file_path(&self, sha: &Sha256) -> PathBuf {
        let mut path = self.texture_base_path();
        path.push(sha.as_hex_string());
        path
    }

    fn index_file_path(&self) -> PathBuf {
        let mut path = self.base_dir.clone();
        path.push("collection.json");
        path
    }

    pub fn new(base_dir: &Path) -> io::Result<DataStore> {
        let mut store = DataStore {
            base_dir: base_dir.into(),

            textures: Default::default(),
            id_index: Default::default(),
            name_index: Default::default(),

            _preview_cache: Default::default(),
        };

        let metadata_file = metadata_file::MetadataFile::load(base_dir)?;

        for texture in metadata_file.textures.iter() {
            match store.insert(texture.clone()) {
                true => (),
                false => {
                    panic!("inserting {:#?} failed !!!", texture); // TODO: What should be done?
                }
            }
        }

        store.garbage_collect()?;
        store.flush_metadata()?;

        Ok(store)
    }

    pub fn texture_by_id(&self, id: &str) -> Option<Texture> {
        self.id_index.get(id).cloned()
    }

    pub fn texture_by_name(&self, id: &str) -> Option<Texture> {
        self.name_index.get(id).cloned()
    }

    pub fn insert(&mut self, texture: Texture) -> bool {
        if self.id_index.contains_key(&texture.id) {
            return false;
        }

        if self.name_index.contains_key(&texture.name) {
            return false;
        }

        if !self.is_texture_file_on_disk(&texture.texture_hash) {
            return false;
        }

        self.id_index.insert(texture.id.clone(), texture.clone());
        self.name_index
            .insert(texture.name.clone(), texture.clone());
        self.textures.insert(texture.clone());

        true
    }

    /// returns true if successful
    pub fn delete(&mut self, tex: &Texture) -> bool {
        if self.textures.remove(tex) {
            // remove
            assert!(self.id_index.remove(&tex.id).is_some());
            assert!(self.name_index.remove(&tex.name).is_some());

            // don't delete cache, since it could be used
            // by other texture.

            true
        } else {
            false
        }
    }

    /// Check if the texture, given by hash, physically exists on the file system.
    pub fn is_texture_file_on_disk(&self, hash: &Sha256) -> bool {
        let file_path = self.texture_file_path(&hash);

        file_path.is_file()
    }

    pub fn read_texture_file_by_hash(&self, hash: &Sha256) -> io::Result<Vec<u8>> {
        use std::fs::*;
        use std::io::*;

        let file_path = self.texture_file_path(&hash);

        let mut file = File::open(file_path)?;
        let mut buffer = Vec::new();
        file.read_to_end(&mut buffer)?;

        Ok(buffer)
    }

    pub fn store_texture_file(&mut self, data: &[u8]) -> io::Result<()> {
        use std::fs;
        use std::io::Write;

        let hash = crate::model::Sha256::from_data(data);

        fs::create_dir_all(&self.texture_base_path())?;
        let file_path = self.texture_file_path(&hash);

        let mut file = fs::File::create(&file_path)?;

        file.write_all(data)
    }

    pub fn flush_metadata(&self) -> io::Result<()> {
        let f = metadata_file::MetadataFile::from_iterator(self.textures.iter());

        f.store(self.index_file_path().as_path())
    }

    pub fn get_texture_preview(
        &mut self,
        _hash: &Sha256,
        _desired_format: TextureFormat,
    ) -> TextureFileResult {
        unimplemented!();
    }

    pub fn borrow_textures(&self) -> impl Iterator<Item = &Texture> {
        self.textures.iter()
    }

    pub fn garbage_collect(&mut self) -> io::Result<()> {
        //unimplemented!()

        /// VERY TODO:
        eprintln!("WARNING: We are sorry the GC isn't implemented yet :'( ");
        Ok(())
    }
}
