use std::fs::{File, create_dir_all};
use std::io::copy;
use std::path::PathBuf;

use chrono::NaiveDateTime;
use zip::read::ZipFile;

use crate::Result;
use crate::extractor::{Entry, Entries, ToteExtractor};

/// ZIP format extractor implementation.
///
/// This extractor handles ZIP archive files.
pub(super) struct Extractor {}

impl ToteExtractor for Extractor {
    fn list(&self, archive_file: PathBuf) -> Result<Entries> {
        let zip_file = File::open(&archive_file).unwrap();
        let mut zip = zip::ZipArchive::new(zip_file).unwrap();

        let mut result = vec![];
        for i in 0..zip.len() {
            let file = zip.by_index(i).unwrap();
            result.push(convert(file));
        }
        Ok(Entries::new(archive_file, result))
    }

    fn perform(&self, archive_file: PathBuf, base: PathBuf) -> Result<()> {
        let zip_file = File::open(archive_file).unwrap();
        let mut zip = zip::ZipArchive::new(zip_file).unwrap();
        for i in 0..zip.len() {
            let mut file = zip.by_index(i).unwrap();
            if file.is_file() {
                log::info!("extracting {} ({} bytes)", file.name(), file.size());
                let dest = base.join(file.name());
                create_dir_all(dest.parent().unwrap()).unwrap();
                let mut out = File::create(dest).unwrap();
                copy(&mut file, &mut out).unwrap();
            }
        }
        Ok(())
    }
}

fn convert<R: std::io::Read>(zfile: ZipFile<R>) -> Entry {
    let name = zfile.name().to_string();
    let compressed_size = zfile.compressed_size();
    let uncompresseed_size = zfile.size();
    let mode = zfile.unix_mode();
    let mtime = match zfile.last_modified() {
        Some(t) => convert_to_datetime(t),
        None => None,
    };
    Entry::builder()
        .name(name)
        .compressed_size(compressed_size)
        .original_size(uncompresseed_size)
        .unix_mode(mode.unwrap())
        .date(mtime)
        .build()
}

fn convert_to_datetime(t: zip::DateTime) -> Option<NaiveDateTime> {
    use chrono::NaiveDate;

    let year = t.year() as i32;
    let month = t.month() as u32;
    let day = t.day() as u32;
    let hour = t.hour() as u32;
    let minute = t.minute() as u32;
    let second = t.second() as u32;
    NaiveDate::from_ymd_opt(year, month, day)
        .unwrap()
        .and_hms_opt(hour, minute, second)
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::path::PathBuf;

    #[test]
    fn test_list_archives() {
        let file = PathBuf::from("../testdata/test.zip");
        let extractor = Extractor {};
        match extractor.list(file) {
            Ok(r) => {
                assert_eq!(r.len(), 19);
                let mut i = r.iter();
                assert_eq!(
                    i.next().map(|t| &t.name),
                    Some("Cargo.toml".to_string()).as_ref()
                );
                assert_eq!(
                    i.next().map(|t| &t.name),
                    Some("build.rs".to_string()).as_ref()
                );
                assert_eq!(
                    i.next().map(|t| &t.name),
                    Some("LICENSE".to_string()).as_ref()
                );
                assert_eq!(
                    i.next().map(|t| &t.name),
                    Some("README.md".to_string()).as_ref()
                );
            }
            Err(_) => assert!(false),
        }
    }

    #[test]
    fn test_extract_archive() {
        let archive_file = PathBuf::from("../testdata/test.zip");
        let opts = crate::ExtractConfig::builder().dest("results/zip").build();
        match crate::extract(archive_file, &opts) {
            Ok(_) => {
                assert!(true);
                assert!(PathBuf::from("results/zip/Cargo.toml").exists());
                std::fs::remove_dir_all(PathBuf::from("results/zip")).unwrap();
            }
            Err(_) => assert!(false),
        };
    }
}
