// This file is part of Nitrogen.
//
// Nitrogen is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Nitrogen is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Nitrogen.  If not, see <http://www.gnu.org/licenses/>.
use crate::{DrawerFileId, DrawerFileMetadata, DrawerInterface};
use anyhow::{ensure, Result};
use memmap::{Mmap, MmapOptions};
use std::{
    borrow::Cow,
    collections::{hash_map::Entry, HashMap},
    ffi::OsStr,
    fs,
    io::{Read, Seek, SeekFrom},
    ops::Range,
    path::PathBuf,
};

const MIN_MAP_SIZE: u64 = 1_048_576;

pub struct DirectoryDrawer {
    name: String,
    priority: i64,
    path: PathBuf,
    index: HashMap<DrawerFileId, String>,
    // cache open files that we read_slice out of, in the expectation that we
    // will want to read other slices subsequently.
    maps: HashMap<DrawerFileId, Mmap>,
}

impl DirectoryDrawer {
    fn populate_from_directory(&mut self, only_extension: Option<&str>) -> Result<()> {
        for (i, entry) in fs::read_dir(&self.path)?.enumerate() {
            let entry = entry?;
            if !entry.file_type()?.is_file() {
                continue;
            }
            if let Some(raw_name) = entry.path().file_name() {
                let name = raw_name.to_string_lossy().to_string();
                if let Some(ext) = only_extension {
                    if !name.ends_with(&ext.to_lowercase()) && !name.ends_with(&ext.to_uppercase())
                    {
                        continue;
                    }
                }
                let id = DrawerFileId::from_u32(i as u32);
                self.index.insert(id, name);
                if entry.metadata()?.len() > MIN_MAP_SIZE {
                    let fp = fs::File::open(entry.path())?;
                    let mmap = unsafe { MmapOptions::new().map(&fp) }?;
                    self.maps.insert(id, mmap);
                }
            }
        }
        Ok(())
    }

    fn from_directory_internal<S: AsRef<OsStr> + ?Sized>(
        priority: i64,
        path_name: &S,
        only_extension: Option<&str>,
    ) -> Result<Box<dyn DrawerInterface>> {
        let path = PathBuf::from(path_name);
        let name = path
            .file_name()
            .expect("a file")
            .to_string_lossy()
            .to_string();
        let mut dd = Self {
            name,
            priority,
            path,
            index: HashMap::new(),
            maps: HashMap::new(),
        };
        dd.populate_from_directory(only_extension)?;
        Ok(Box::new(dd))
    }

    pub fn from_directory_with_extension<S: AsRef<OsStr> + ?Sized>(
        priority: i64,
        path_name: &S,
        only_extension: &str,
    ) -> Result<Box<dyn DrawerInterface>> {
        Self::from_directory_internal(priority, path_name, Some(only_extension))
    }

    pub fn from_directory<S: AsRef<OsStr> + ?Sized>(
        priority: i64,
        path_name: &S,
    ) -> Result<Box<dyn DrawerInterface>> {
        Self::from_directory_internal(priority, path_name, None)
    }
}

impl DrawerInterface for DirectoryDrawer {
    fn index(&self) -> Result<HashMap<DrawerFileId, String>> {
        Ok(self.index.clone())
    }

    fn priority(&self) -> i64 {
        self.priority
    }

    fn name(&self) -> &str {
        &self.name
    }

    fn stat(&self, id: DrawerFileId) -> Result<DrawerFileMetadata> {
        ensure!(self.index.contains_key(&id), "file not found");
        let mut global_path = self.path.clone();
        global_path.push(&self.index[&id]);
        let meta = fs::metadata(&global_path)?;
        Ok(DrawerFileMetadata {
            drawer_file_id: id,
            name: self.index[&id].clone(),
            compression: None,
            packed_size: meta.len(),
            unpacked_size: meta.len(),
            path: global_path.to_string_lossy().into(),
        })
    }

    fn read(&self, id: DrawerFileId) -> Result<Cow<[u8]>> {
        ensure!(self.index.contains_key(&id), "file not found");
        let mut global_path = self.path.clone();
        global_path.push(&self.index[&id]);
        let mut fp = fs::File::open(&global_path)?;
        let mut content = Vec::new();
        fp.read_to_end(&mut content)?;
        Ok(Cow::from(content))
    }

    fn read_slice(&self, id: DrawerFileId, extent: Range<usize>) -> Result<Cow<[u8]>> {
        ensure!(self.index.contains_key(&id), "file not found");
        let mut global_path = self.path.clone();
        global_path.push(&self.index[&id]);
        let mut fp = fs::File::open(&global_path)?;
        fp.seek(SeekFrom::Start(extent.start as u64))?;
        let mut content = vec![0u8; extent.end - extent.start];
        fp.read_exact(&mut content)?;
        Ok(Cow::from(content))
    }

    fn read_mapped_slice(&mut self, id: DrawerFileId, extent: Range<usize>) -> Result<&[u8]> {
        ensure!(self.index.contains_key(&id), "file not found");
        if let Entry::Vacant(e) = self.maps.entry(id) {
            let mut global_path = self.path.clone();
            global_path.push(&self.index[&id]);
            let fp = fs::File::open(&global_path)?;
            let mmap = unsafe { MmapOptions::new().map(&fp) }?;
            e.insert(mmap);
        }
        Ok(&self.maps[&id][extent])
    }
}
