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

use chrono::DateTime;
use delharc::{LhaDecodeReader, LhaHeader};

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

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

impl ToteExtractor for Extractor {
    fn list(&self, archive_file: PathBuf) -> Result<Entries> {
        let mut result = vec![];
        let mut reader = delharc::parse_file(&archive_file).map_err(Error::IO)?;
        loop {
            let header = reader.header();
            if !header.is_directory() {
                result.push(convert(header));
            }
            match reader.next_file() {
                Ok(r) => {
                    if !r {
                        break;
                    }
                }
                Err(e) => return Err(Error::Fatal(Box::new(e))),
            }
        }
        Ok(Entries::new(archive_file, result))
    }

    fn perform(&self, archive_file: PathBuf, base: PathBuf) -> Result<()> {
        let mut reader = delharc::parse_file(archive_file).map_err(Error::IO)?;
        let mut errs = vec![];
        loop {
            if let Err(e) = write_data_impl(&mut reader, &base) {
                errs.push(e);
            }
            match reader.next_file() {
                Ok(r) => {
                    if !r {
                        break;
                    }
                }
                Err(e) => return Err(Error::Fatal(Box::new(e))),
            }
        }
        Error::error_or((), errs)
    }
}

fn write_data_impl(reader: &mut LhaDecodeReader<File>, base: &Path) -> Result<()> {
    let header = reader.header();
    let name = header.parse_pathname();
    let dest = base.join(&name);
    if reader.is_decoder_supported() {
        log::info!("extracting {:?} ({} bytes)", &name, header.original_size);
        create_dir_all(dest.parent().unwrap()).unwrap();
        let mut dest = File::create(dest).map_err(Error::IO)?;
        copy(reader, &mut dest).map_err(Error::IO)?;
        if let Err(e) = reader.crc_check() {
            return Err(Error::Fatal(Box::new(e)));
        };
    } else if !header.is_directory() {
        log::info!(
            "{name:?}: unsupported compression method ({:?})",
            header.compression
        );
    }
    Ok(())
}

fn convert(h: &LhaHeader) -> Entry {
    let name = h.parse_pathname().to_str().unwrap().to_string();
    let compressed_size = h.compressed_size;
    let original_size = h.original_size;
    let mtime = h.last_modified as i64;
    let dt = DateTime::from_timestamp(mtime, 0)
        .map(|dt| dt.naive_local());
    Entry::builder()
        .name(name)
        .compressed_size(compressed_size)
        .original_size(original_size)
        .date(dt)
        .build()
}

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

    #[test]
    fn test_list_archives() {
        let file = PathBuf::from("../testdata/test.lzh");
        let extractor = Extractor {};
        match extractor.list(file) {
            Ok(r) => {
                let r = r.iter().map(|e| e.name.clone()).collect::<Vec<_>>();
                assert_eq!(r.len(), 23);
                assert_eq!(r.get(0), Some("Cargo.toml".to_string()).as_ref());
                assert_eq!(r.get(1), Some("LICENSE".to_string()).as_ref());
                assert_eq!(r.get(2), Some("README.md".to_string()).as_ref());
                assert_eq!(r.get(3), Some("build.rs".to_string()).as_ref());
            }
            Err(_) => assert!(false),
        }
    }

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