use androscalpel_serializer::Serializable;
use std::collections::HashMap;
use std::env;
use std::fs;
use std::fs::File;
use std::io::{Read, Seek};
use std::path::Path;
use std::process::Command;

pub mod apk_signing_block;
pub mod compression;
mod cp437;
pub mod data_descriptor;
pub mod end_of_central_directory;
pub mod error;
pub mod extra_fields;
pub mod file_header;
pub mod local_file_header;
pub mod zip_reader;
pub mod zip_writer;

use data_descriptor::DataDescriptor;
use extra_fields::{ExtraField, Zip64ExtraField};
use file_header::FileHeader;
use local_file_header::LocalFileHeader;
pub use zip_reader::ZipFileReader;
pub use zip_writer::ZipFileWriter;

#[derive(Debug, Clone, PartialEq, Eq, Serializable, Default)]
pub struct Signature(pub u32);

pub enum Encoding {
    CP437,
    UTF8,
}

pub mod general_purpose_flags {
    pub const MASK_ENCRYPTED: u16 = 1 << 0;
    pub const MASK_COMPRESS_OPTION_1: u16 = 1 << 1;
    pub const MASK_COMPRESS_OPTION_2: u16 = 1 << 2;
    pub const MASK_USE_DATA_DESCRIPTOR: u16 = 1 << 3;
    pub const MASK_STRONG_ENCRYPTION: u16 = 1 << 6;
    pub const MASK_UTF8_FILENAME: u16 = 1 << 11;
    pub const MASK_ENCRYPTED_CENTRAL_DIR: u16 = 1 << 13;
}

pub mod external_file_attributes {
    pub const FIFO_FILE: u32 = 0b00010000 << 24;
    // #define S_IFCHR  0020000  /* character special */ ?
    pub const DIR_FILE: u32 = 0b01000000 << 24;
    // #define S_IFBLK  0060000  /* block special */ ?
    pub const SYMBOLIC_LINK_FILE: u32 = 0b10100000 << 24;
    pub const SOCKET_FILE: u32 = 0b11000000 << 24;
    pub const REGULAR_FILE: u32 = 0b10000000 << 24;
    pub const MASK_FILE_TYPE: u32 = 0b11110000 << 24;
    pub const SETUID: u32 = 0b0000100000000000 << 16;
    pub const SETGID: u32 = 0b0000010000000000 << 16;
    pub const STICKY: u32 = 0b0000001000000000 << 16;
    pub const PERM_UR: u32 = 0b0000000100000000 << 16;
    pub const PERM_UW: u32 = 0b0000000010000000 << 16;
    pub const PERM_UX: u32 = 0b0000000001000000 << 16;
    pub const PERM_GR: u32 = 0b0000000000100000 << 16;
    pub const PERM_GW: u32 = 0b0000000000010000 << 16;
    pub const PERM_GX: u32 = 0b0000000000001000 << 16;
    pub const PERM_OR: u32 = 0b0000000000000100 << 16;
    pub const PERM_OW: u32 = 0b0000000000000010 << 16;
    pub const PERM_OX: u32 = 0b0000000000000001 << 16;
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FileInfo {
    pub local_header: LocalFileHeader,
    pub header: FileHeader,
    pub data_descriptor: Option<DataDescriptor>,
}

impl FileInfo {
    pub fn get_name(&self) -> String {
        self.header.get_name()
    }
    pub fn set_name(&mut self, name: &str) {
        self.header.set_name(name);
        self.local_header.set_name(name);
    }
    pub fn external_file_attributes_set_flag(&mut self, flag: u32) {
        self.header.external_file_attributes_set_flag(flag);
    }
    pub fn external_file_attributes_unset_flag(&mut self, flag: u32) {
        self.header.external_file_attributes_unset_flag(flag);
    }
    pub fn set_file_type(&mut self, file_type: u32) {
        self.header.set_file_type(file_type);
    }
    pub fn get_offset_local_header(&self) -> u64 {
        self.header.get_offset_local_header()
    }
    pub fn get_compressed_size(&self) -> u64 {
        self.header.get_compressed_size()
    }
    pub fn get_file_offset(&self) -> u64 {
        self.get_offset_local_header() + self.local_header.size() as u64
    }
    pub fn set_offset_local_header(&mut self, new_offset: u64) {
        let use_z64 = new_offset > u32::MAX as u64;
        for extra in &mut self.local_header.extra_field {
            if let ExtraField::Zip64(extra) = extra {
                if use_z64 {
                    extra.offset_header = Some(new_offset);
                } else {
                    extra.offset_header = None;
                }
            }
        }
        let mut z64found = false;
        for extra in &mut self.header.extra_field {
            if let ExtraField::Zip64(extra) = extra {
                if use_z64 {
                    extra.offset_header = Some(new_offset);
                } else {
                    extra.offset_header = None;
                }
                z64found = true;
            }
        }
        if use_z64 && !z64found {
            self.header
                .extra_field
                .push(ExtraField::Zip64(Zip64ExtraField {
                    original_size: Some(self.header.uncompressed_size as u64),
                    compressed_size: Some(self.header.compressed_size as u64),
                    offset_header: Some(new_offset),
                    disk_number: None,
                }));
            self.header.uncompressed_size = u32::MAX;
            self.header.compressed_size = u32::MAX;
        }
        if use_z64 {
            self.header.offset_local_header = u32::MAX;
        } else {
            self.header.offset_local_header = new_offset as u32;
        }
    }
}

/// Replace the dex files a an apk an resigned the apk.
///
/// # Warning
///
/// For now, only jks keystore are allowed.
///
/// The `zipalign` and `apksigner` args allow to use a specific version of the
/// tools instead of the one in the PATH (if it even exist)
pub fn replace_dex(
    apk: impl AsRef<Path>,
    dst: impl AsRef<Path>,
    dexfiles: &mut [impl Read + Seek],
    keystore: impl AsRef<Path>, // TODO enum for handling p11 and generating a random cert
    // `keytool -genkey -v -keystore KEYSTORE.jks -keyalg RSA -keysize
    // 2048 -validity 10000 -alias ALIAS`
    zipalign: Option<impl AsRef<Path>>,
    apksigner: Option<impl AsRef<Path>>,
    additionnal_files: Option<HashMap<String, Option<impl Read + Seek>>>,
) {
    let zipalign = if let Some(path) = &zipalign {
        path.as_ref().as_os_str()
    } else {
        "zipalign".as_ref()
    };
    let apksigner = if let Some(path) = &apksigner {
        path.as_ref().as_os_str()
    } else {
        "apksigner".as_ref()
    };
    let tmp_dir = env::temp_dir().join(format!("apk_frauder_{:x}", rand::random::<u128>()));
    let unaligned_path = tmp_dir.join("stripped.apk");
    let aligned_path = tmp_dir.join("aligned.apk");
    fs::create_dir_all(&tmp_dir).expect("Failed to create temporary directory");

    let file = File::open(apk).expect("failed to open file");
    let mut apk = ZipFileReader::new(file);
    let file = File::create(&unaligned_path).expect("failed to create file");
    let mut apk_out = ZipFileWriter::new(file, apk.zip64_end_of_central_directory.clone());

    let mut file_info_ref = (*apk
        .get_classes_file_info()
        .first()
        .expect("No dex file found in apk"))
    .clone();

    apk.unlink_signature_files();
    apk.unlink_bytecode_files();
    if let Some(additionnal_files) = &additionnal_files {
        apk.files
            .retain(|file| additionnal_files.get(&file.get_name()).is_none());
    }
    for f in apk.files.clone() {
        apk_out.insert_file_from_zip(f, &mut apk);
    }
    for (i, mut dex) in dexfiles.iter_mut().enumerate() {
        if i == 0 {
            file_info_ref.set_name("classes.dex");
        } else {
            file_info_ref.set_name(&format!("classes{}.dex", i + 1));
        }
        apk_out.insert_file(
            &mut dex,
            file_info_ref.header.clone(),
            Some(file_info_ref.local_header.clone()),
            file_info_ref.data_descriptor.clone(),
        );
    }
    if let Some(mut additionnal_files) = additionnal_files {
        for (name, data) in &mut additionnal_files {
            file_info_ref.set_name(name);
            if let Some(data) = data.as_mut() {
                apk_out.insert_file(
                    data,
                    file_info_ref.header.clone(),
                    Some(file_info_ref.local_header.clone()),
                    file_info_ref.data_descriptor.clone(),
                );
            }
        }
    }
    apk_out.write_central_directory();
    // TODO: we can probably do that ourself an spare ourself the trouble of finding zipalign
    Command::new(zipalign)
        .arg("-v")
        .arg("-p")
        .arg("4")
        .arg(unaligned_path.as_os_str())
        .arg(aligned_path.as_os_str())
        .status()
        .unwrap();

    Command::new(apksigner)
        .arg("sign")
        .arg("--ks")
        .arg(keystore.as_ref().as_os_str())
        .arg("--out")
        .arg(dst.as_ref().as_os_str())
        .arg(aligned_path.as_os_str())
        .status()
        .unwrap();
    fs::remove_dir_all(tmp_dir).expect("Failled to remove tmp dir");
}
