use crate::exec::{macho::Macho, Architecture, ExecutableFile, OperatingSystem, Sections};
use crate::utils::{bytes_offset_match, u32_from_offset};
use crate::{Ordering, TypeMagic};

use std::fmt::{Display, Formatter};

use anyhow::{bail, Result};
use chrono::{DateTime, Utc};

const MAGIC: [u8; 4] = [0xCA, 0xFE, 0xBA, 0xBE];

/// Fat Mach-O files contain executable code for more than one architecture, allowing the
/// same binary to be run on different hardware, such as the same file working on
/// PowerPC, Intel, and Apple Silicon machines.
///
/// This format is an array of Mach-O files. However, the magic number is also used for Java
/// class files, so we need to make sure the amount of stored binaries makes sense. Too high, and
/// it's probably the Java class version and not the number of contained Mach Objects.
#[derive(Clone, Debug)]
pub struct FatMacho<'a> {
    /// The embedded Mach-O files within
    pub binaries: Vec<Macho<'a>>,

    /// If the binary has extra data after the last section, could be used to hide something
    pub has_overlay: Option<bool>,

    /// The array containing the raw bytes used to parse this program
    pub contents: &'a [u8],
}

impl<'a> FatMacho<'a> {
    pub fn from(contents: &'a [u8]) -> Result<Self> {
        if !bytes_offset_match(contents, 0, &MAGIC) {
            bail!("Not a Fat Mach-O file");
        }

        let contained_binaries = u32_from_offset(contents, 4, Ordering::BigEndian) as usize;

        if contained_binaries > 0x20 {
            // Might be a Java .class file
            // https://stackoverflow.com/questions/73546728/magic-value-collision-between-macho-fat-binaries-and-java-class-files
            bail!("Not a Fat Mach-O file");
        }

        let mut binaries = Vec::with_capacity(contained_binaries);
        let mut offset_counter = 8;
        let mut has_overlay = None;
        for contained_binary_offset in 0..contained_binaries {
            let offset =
                u32_from_offset(contents, offset_counter + 8, Ordering::BigEndian) as usize;
            let size = u32_from_offset(contents, offset_counter + 12, Ordering::BigEndian) as usize;
            binaries.push(Macho::from(&contents[offset..offset + size])?);

            if contained_binary_offset == contained_binaries - 1 {
                // See if there is extra space in the binary after the last section
                has_overlay = Some(offset + size < contents.len());
            }

            offset_counter += 20;
        }

        Ok(Self {
            binaries,
            has_overlay,
            contents,
        })
    }
}

// TODO: Fix up `ExecutableFile` for `FatMacho`
impl<'a> ExecutableFile for FatMacho<'a> {
    fn type_name(&self) -> String {
        "Fat Mach-O".into()
    }

    fn architecture(&self) -> &Architecture {
        todo!()
    }

    fn pointer_size(&self) -> usize {
        self.binaries.first().unwrap().pointer_size()
    }

    fn operating_system(&self) -> &OperatingSystem {
        self.binaries.first().unwrap().operating_system()
    }

    fn compiled_timestamp(&self) -> Option<DateTime<Utc>> {
        None
    }

    fn num_sections(&self) -> u32 {
        self.binaries.iter().map(|b| b.num_sections()).sum()
    }

    fn sections(&self) -> Result<&Sections> {
        self.binaries.first().unwrap().sections()
    }
}

impl<'a> TypeMagic for FatMacho<'a> {
    const MAGIC: &'static [&'static [u8]] = &[&MAGIC];
}

impl<'a> Display for FatMacho<'a> {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        writeln!(
            f,
            "Fat Mach-O containing {} architectures:",
            self.binaries.len()
        )?;
        for bin in self.binaries.iter() {
            writeln!(f, "{bin}")?;
        }
        if self.has_overlay == Some(true) {
            writeln!(f, "\tHas extra bytes at the end (overlay).")?;
        }
        writeln!(f, "\tTotal Size: {}", self.contents.len())
    }
}

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

    #[test]
    fn three_arch() {
        const BYTES: &[u8] = include_bytes!("../../../testdata/macho/macho_fat_arm64_x86_64");

        let macho = FatMacho::from(BYTES).unwrap();
        assert_eq!(macho.binaries.len(), 2);
    }

    #[test]
    fn four_arch() {
        const BYTES: &[u8] =
            include_bytes!("../../../testdata/macho/macho_fat_arm64_ppc_ppc64_x86_64");

        let macho = FatMacho::from(BYTES).unwrap();
        assert_eq!(macho.binaries.len(), 4);
    }

    #[test]
    fn java() {
        const BYTES: &[u8] = include_bytes!("../../../testdata/class/Hello.class");
        assert!(FatMacho::from(BYTES).is_err());
    }
}
