use log::{info, warn};
use std::collections::HashMap;
use std::io::{Read, Seek, SeekFrom};

use crate::{
    apk_signing_block::ApkSigningBlock,
    apk_signing_block::Magic,
    compression::CompressionMethod,
    data_descriptor::{DataDescriptor, DataDescriptor32, DataDescriptor64},
    end_of_central_directory::EndCentralDirectory,
    end_of_central_directory::Zip64EndCentralDirectory,
    end_of_central_directory::Zip64EndCentralDirectoryLocator,
    general_purpose_flags, FileHeader, FileInfo, LocalFileHeader, Signature,
};
use androscalpel_serializer::Serializable;
use flate2::read::DeflateDecoder;

#[derive(Debug, PartialEq, Eq)]
pub struct ZipFileReader<T: Read + Seek> {
    pub end_of_central_directory: EndCentralDirectory,
    pub zip64_end_of_central_directory: Option<Zip64EndCentralDirectory>,
    pub files: Vec<FileInfo>,
    pub apk_sign_block: Option<ApkSigningBlock>,
    pub data: T,
}

impl<T: Read + Seek> ZipFileReader<T> {
    pub fn new(mut reader: T) -> Self {
        let end_of_central_directory_off =
            Self::get_end_of_central_directory_offset(&mut reader).unwrap();
        reader
            .seek(SeekFrom::Start(end_of_central_directory_off))
            .unwrap();
        let end_of_central_directory = EndCentralDirectory::deserialize(&mut reader).unwrap();
        reader
            .seek(SeekFrom::Start(
                end_of_central_directory_off - Zip64EndCentralDirectoryLocator::SIZE as u64,
            ))
            .unwrap();
        let zip64_ecd_locator = Zip64EndCentralDirectoryLocator::deserialize(&mut reader).ok();
        let zip64_end_of_central_directory = if let Some(zip64_ecd_locator) = zip64_ecd_locator {
            assert_eq!(
                zip64_ecd_locator.disk_number_of_zip64_end_central_directory_start,
                0
            );
            assert!(zip64_ecd_locator.total_number_of_disks <= 1);
            let zip64_edc_record_off =
                zip64_ecd_locator.offset_zip64_end_of_central_directory_record;
            reader.seek(SeekFrom::Start(zip64_edc_record_off)).unwrap();
            Zip64EndCentralDirectory::deserialize(&mut reader).ok()
        } else {
            None
        };
        //println!("{:#?}", end_of_central_directory);

        // At this point python's ziplib recompute the location of the central directory from the
        // location of the end of central directory in case the zip was concanated after a file.
        // We probably don't need that for now.
        let mut zip_file = Self {
            end_of_central_directory,
            zip64_end_of_central_directory,
            data: reader,
            files: vec![],
            apk_sign_block: None,
        };
        zip_file
            .data
            .seek(SeekFrom::Start(zip_file.get_cd_offset()))
            .unwrap();

        let mut size_read = 0;
        let cd_size = zip_file.get_cd_size();
        while size_read < cd_size {
            let header = FileHeader::deserialize(&mut zip_file.data).unwrap();
            //println!("{:#?}", header);
            size_read += header.size() as u64;
            let pos_in_dir = zip_file.data.stream_position().unwrap();
            if header.general_purpose_flags & general_purpose_flags::MASK_ENCRYPTED_CENTRAL_DIR != 0
            {
                panic!("Central directory encryption not supported");
            }
            zip_file
                .data
                .seek(SeekFrom::Start(header.get_offset_local_header()))
                .unwrap();
            let local_header = LocalFileHeader::deserialize(&mut zip_file.data).unwrap();
            let data_descriptor = if (local_header.general_purpose_flags
                & general_purpose_flags::MASK_USE_DATA_DESCRIPTOR
                != 0)
                || (header.general_purpose_flags & general_purpose_flags::MASK_USE_DATA_DESCRIPTOR
                    != 0)
            {
                warn!("Data Descriptor support is experimental");
                zip_file
                    .data
                    .seek(SeekFrom::Current(header.compressed_size as i64))
                    .unwrap();
                if zip_file.zip64_end_of_central_directory.is_some() {
                    Some(DataDescriptor::Zip64(
                        DataDescriptor64::deserialize(&mut zip_file.data).unwrap(),
                    ))
                } else {
                    Some(DataDescriptor::Zip32(
                        DataDescriptor32::deserialize(&mut zip_file.data).unwrap(),
                    ))
                }
            } else {
                None
            };
            zip_file.data.seek(SeekFrom::Start(pos_in_dir)).unwrap();
            zip_file.files.push(FileInfo {
                local_header,
                header,
                data_descriptor,
            });
        }
        assert_eq!(size_read, cd_size);
        if zip_file.get_cd_offset() > 16 {
            zip_file
                .data
                .seek(SeekFrom::Start(zip_file.get_cd_offset() - 16))
                .unwrap();
            let magic = Magic::deserialize(&mut zip_file.data).unwrap();
            if magic == ApkSigningBlock::MAGIC {
                zip_file
                    .data
                    .seek(SeekFrom::Start(zip_file.get_cd_offset() - 16 - 8))
                    .unwrap();
                let block_size = u64::deserialize(&mut zip_file.data).unwrap();
                zip_file
                    .data
                    .seek(SeekFrom::Start(zip_file.get_cd_offset() - block_size - 8))
                    .unwrap();

                zip_file.apk_sign_block = ApkSigningBlock::deserialize(&mut zip_file.data).ok();
            }
        }

        zip_file
    }

    pub fn is_zip64(&self) -> bool {
        self.zip64_end_of_central_directory.is_some()
    }

    pub fn get_disk_num(&self) -> u32 {
        if let Some(zip64_end_of_central_directory) = &self.zip64_end_of_central_directory {
            zip64_end_of_central_directory.number_of_this_disk
        } else {
            self.end_of_central_directory.disk_number as u32
        }
    }

    pub fn get_disk_ed_start(&self) -> u32 {
        if let Some(zip64_end_of_central_directory) = &self.zip64_end_of_central_directory {
            zip64_end_of_central_directory.disk_number_of_central_directory_start
        } else {
            self.end_of_central_directory
                .disk_number_of_central_directory_start as u32
        }
    }

    pub fn get_number_entries_on_disk(&self) -> u64 {
        if let Some(zip64_end_of_central_directory) = &self.zip64_end_of_central_directory {
            zip64_end_of_central_directory.number_entry_in_central_directory_on_this_disk
        } else {
            self.end_of_central_directory
                .number_of_entries_in_central_directory_on_disk as u64
        }
    }

    pub fn get_number_entries(&self) -> u64 {
        if let Some(zip64_end_of_central_directory) = &self.zip64_end_of_central_directory {
            zip64_end_of_central_directory.number_entry_in_central_directory
        } else {
            self.end_of_central_directory
                .number_of_entries_in_central_directory as u64
        }
    }

    pub fn get_cd_size(&self) -> u64 {
        if let Some(zip64_end_of_central_directory) = &self.zip64_end_of_central_directory {
            zip64_end_of_central_directory.size_of_central_directory
        } else {
            self.end_of_central_directory.size_central_directory as u64
        }
    }

    pub fn get_cd_offset(&self) -> u64 {
        if let Some(zip64_end_of_central_directory) = &self.zip64_end_of_central_directory {
            zip64_end_of_central_directory.offset_central_directory
        } else {
            self.end_of_central_directory.offset_central_directory as u64
        }
    }

    pub fn get_end_of_central_directory_offset(reader: &mut T) -> Option<u64> {
        let file_size = reader.seek(SeekFrom::End(0)).unwrap();
        let mut sig = Signature::default();
        let mut comment_size = 0;
        while sig != EndCentralDirectory::SIGNATURE {
            reader
                .seek(SeekFrom::End(
                    -(EndCentralDirectory::MIN_SIZE as i64) - comment_size,
                ))
                .unwrap();
            sig = Signature::deserialize(reader).unwrap();
            comment_size += 1;
            if comment_size > 65536
                || comment_size as usize + EndCentralDirectory::MIN_SIZE > file_size as usize
            {
                return None;
            }
        }
        comment_size -= 1;
        Some(file_size - comment_size as u64 - EndCentralDirectory::MIN_SIZE as u64)
    }

    pub fn get_file_names(&self) -> Vec<String> {
        self.files.iter().map(|f| f.get_name()).collect()
    }

    /// List files used to signe jar file (apk signature v1) found in the zip file:
    /// META-INF/MANIFEST.MF
    /// META-INF/*.SF
    /// META-INF/*.DSA
    /// META-INF/*.RSA
    /// META-INF/SIG-*
    pub fn get_jar_sig_files(&self) -> Vec<&FileInfo> {
        self.files
            .iter()
            .filter(|file| match_v1_signature_file(&file.get_name()))
            .collect()
    }

    /// Test if the zipfile contains files used to signe jar file (apk signature v1):
    /// META-INF/MANIFEST.MF
    /// META-INF/*.SF
    /// META-INF/*.DSA
    /// META-INF/*.RSA
    /// META-INF/SIG-*
    ///
    /// TODO: there is a field `X-Android-APK-Signed` in .SF that indicate the use of v2 and v3
    /// (and v4?) signature.
    pub fn is_signed_v1(&self) -> bool {
        !self.get_jar_sig_files().is_empty()
    }

    /// Test if the zipfile as apk signature block.
    pub fn is_signed_v2(&self) -> bool {
        self.apk_sign_block.is_some()
    }

    /// Remove v1 signature files from the file index.
    ///
    /// This function does no modify the original file, only the index store
    /// in the struct.
    pub fn unlink_signature_files(&mut self) {
        self.files
            .retain(|file| !match_v1_signature_file(&file.get_name()));
    }

    /// Unlink bytecode files.
    ///
    /// This function does no modify the original file, only the index store
    /// in the struct.
    pub fn unlink_bytecode_files(&mut self) {
        self.files
            .retain(|file| !match_dexfile_name(&file.get_name()));
    }

    pub fn check_holes(&self) {
        let mut files: Vec<&FileInfo> = self.files.iter().collect();
        files.sort_by_key(|f| f.get_offset_local_header());
        let mut lst_offset = 0;
        for file in files.iter() {
            if file.get_offset_local_header() != lst_offset {
                info!(
                    "Hole in zip before {} between 0x{:x} and 0x{:x}",
                    file.get_name(),
                    lst_offset,
                    file.get_offset_local_header()
                );
            }
            lst_offset += file.local_header.size() as u64;
            lst_offset += file.get_compressed_size();
        }
        if let Some(apk_sign_block) = &self.apk_sign_block {
            let apk_sb_off = self.get_cd_offset() - apk_sign_block.size() as u64;
            if apk_sb_off != lst_offset {
                info!(
                    "Hole in zip before apk signing block, between 0x{:x} and 0x{:x}",
                    lst_offset, apk_sb_off
                );
            }

            lst_offset = self.get_cd_offset();
        }
        if self.get_cd_offset() != lst_offset {
            info!(
                "Hole in zip before central directory between 0x{:x} and 0x{:x}",
                lst_offset,
                self.get_cd_offset()
            );
        }
    }

    pub fn get_bin(&mut self, offset: u64, size: usize) -> Vec<u8> {
        self.data.seek(SeekFrom::Start(offset)).unwrap();
        let mut data = vec![0u8; size];
        self.data.read_exact(&mut data).unwrap();
        /*
        for _ in 0..size {
            data.push(u8::deserialize(&mut self.data).unwrap());
        }
        */
        data
    }

    pub fn read_file_as_vec(&mut self, name: &str) -> Vec<u8> {
        let file = self.get_file_info(name).unwrap();
        let offset = file.get_file_offset();
        let size_c = file.header.compressed_size as usize;
        let size = file.header.uncompressed_size as usize;
        let compression_method = file.header.compression_method;
        let mut data = vec![0u8; size_c];
        self.data.seek(SeekFrom::Start(offset)).unwrap();
        self.data.read_exact(&mut data).unwrap();
        match compression_method {
            CompressionMethod::Stored => {}
            CompressionMethod::Deflated => {
                let mut decomp_data = vec![0u8; size];
                let mut deflater = DeflateDecoder::new(&data[..]);
                deflater.read_exact(&mut decomp_data).unwrap();
                data = decomp_data
            }
            _ => unimplemented!(),
        }
        data
    }

    pub fn get_file_info(&self, name: &str) -> Option<&FileInfo> {
        self.files.iter().find(|&file| file.get_name() == name)
    }

    pub fn get_classes_file_info(&self) -> Vec<&FileInfo> {
        let files_map: HashMap<String, &FileInfo> = self
            .files
            .iter()
            .by_ref()
            .filter(|&file| match_dexfile_name(&file.get_name()))
            .map(|file| (file.get_name(), file))
            .collect();
        let mut files = vec![];
        let mut i = 0;
        loop {
            let name = if i == 0 {
                "classes.dex".into()
            } else {
                format!("classes{}.dex", i + 1)
            };
            if let Some(file) = files_map.get(&name) {
                files.push(*file);
            } else {
                break;
            }
            i += 1;
        }
        files
    }
}

// Not worth a regex
/// Check if a file is a bytecode file (from its name)
fn match_dexfile_name(name: &str) -> bool {
    if name.len() < 11 {
        return false;
    }
    if &name[0..7] != "classes" {
        return false;
    }
    if &name[name.len() - 4..name.len()] != ".dex" {
        return false;
    }

    name.len() == 11 || name[7..name.len() - 4].parse::<u32>().is_ok()
}

/// Check if a file is used to sign the APK with signature scheme v1.
fn match_v1_signature_file(name: &str) -> bool {
    let l = name.len();
    (name == "META-INF/MANIFEST.MF")
        || (l >= 13 && &name[..9] == "META-INF/" && &name[l - 3..] == ".SF")
        || (l >= 14 && &name[..9] == "META-INF/" && &name[l - 4..] == ".DSA")
        || (l >= 14 && &name[..9] == "META-INF/" && &name[l - 4..] == ".RSA")
        || (l >= 14 && &name[..13] == "META-INF/SIG-")
}
