use crate::exec::{Architecture, ExecutableFile, OperatingSystem, Section, Sections};
use crate::utils::{
    bytes_offset_match, i32_from_offset, string_from_offset, u16_from_offset, u32_from_offset,
};
use crate::{Ordering, TypeMagic};

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

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

// Documentation:
// https://web.archive.org/web/20020219190852/http://developer.apple.com/techpubs/mac/runtimehtml/RTArch-91.html#HEADING=91-0

const MAGIC: [u8; 8] = [0x4a, 0x6f, 0x79, 0x21, 0x70, 0x65, 0x66, 0x66]; // Joy!peff
const PWPC: [u8; 4] = [0x70, 0x77, 0x70, 0x63];
const M68K: [u8; 4] = [0x6d, 0x36, 0x38, 0x6b];

const HEADER_SIZE: usize = 40;
const SECTION_HEADER_SIZE: usize = 28;

/// The struct for [Preferred Executables](https://en.wikipedia.org/wiki/Preferred_Executable_Format).
///
/// This was the binary format for "Classic" Mac OS, and BeOS on PowerPC. Some of the data is only
/// on the "resource fork", which is not available on modern systems, so we can't the entire file. :(
pub struct Pef<'a> {
    /// Instruction set architecture for this binary
    pub arch: Architecture,

    /// Byte ordering for this binary
    pub ordering: Ordering,

    /// Operating System for this binary, likely Classic Mac OS
    pub os: OperatingSystem,

    /// Sections of this binary
    pub sections: Option<Sections<'a>>,

    /// Seconds since 1 January 1904
    pub timestamp: u32,

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

#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub struct SectionHeader {
    /// Location in the file for the section name, or -1 if the section is unnamed
    pub name_offset: i32,

    /// Linker's preferred memory address for loading the binary
    pub default_address: u32,

    /// Total section size in memory at run-time
    pub total_size: u32,

    /// Size of the executable code, or data to be initialized at run-time after decompression
    pub unpacked_size: u32,

    /// Size of the section
    pub packed_size: u32,

    /// Location in the file where the section begins
    pub container_offset: u32,

    /// Attributes of the section
    pub section_kind: u8,

    /// Indicates how data might be shared at run-time
    pub share_kind: u8,

    /// Alignment of bytes in memory
    pub alignment: u8,

    /// Reserved, should be zero
    pub reserved: u8,
}

impl AsRef<[u8; size_of::<Self>()]> for SectionHeader {
    fn as_ref(&self) -> &[u8; size_of::<Self>()] {
        unsafe { std::mem::transmute::<_, &[u8; size_of::<Self>()]>(self) }
    }
}

impl SectionHeader {
    pub fn from(contents: &[u8]) -> Self {
        Self {
            name_offset: i32_from_offset(contents, 0, Ordering::BigEndian),
            default_address: u32_from_offset(contents, 4, Ordering::BigEndian),
            total_size: u32_from_offset(contents, 8, Ordering::BigEndian),
            unpacked_size: u32_from_offset(contents, 12, Ordering::BigEndian),
            packed_size: u32_from_offset(contents, 16, Ordering::BigEndian),
            container_offset: u32_from_offset(contents, 20, Ordering::BigEndian),
            section_kind: contents[24],
            share_kind: contents[25],
            alignment: contents[26],
            reserved: contents[27],
        }
    }
}

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

        let arch = {
            if bytes_offset_match(contents, 8, &PWPC) {
                Architecture::PowerPC
            } else if bytes_offset_match(contents, 8, &M68K) {
                Architecture::M68k
            } else {
                Architecture::Unknown
            }
        };

        let section_count = u16_from_offset(contents, 32, Ordering::BigEndian);
        let inst_section_count = u16_from_offset(contents, 34, Ordering::BigEndian);

        let mut sections = Sections::default();
        for section_index in 0..(section_count + inst_section_count) as usize {
            // There seems to be an issue after "section_count" number of sections where
            // the sizes or needed offset value changes, and the incoming values don't
            // match what one would expect when looking at the binary with a hex editor.
            let offset_this_section = HEADER_SIZE + section_index * SECTION_HEADER_SIZE;
            if offset_this_section > contents.len() {
                break;
            }
            let this_section = SectionHeader::from(
                &contents[offset_this_section..offset_this_section + HEADER_SIZE],
            );

            let section_name = {
                if this_section.name_offset > 0 {
                    string_from_offset(contents, this_section.name_offset as usize)
                } else {
                    format!("Unnamed section {section_index}")
                }
            };

            sections.0.push(Section {
                name: section_name,
                is_executable: this_section.section_kind == 0 || this_section.section_kind == 8,
                size: this_section.packed_size as usize,
                offset: this_section.container_offset as usize,
                virtual_size: 0,
                virtual_address: 0,
                data: None,
                entropy: 0.0,
            });
        }

        Ok(Self {
            arch,
            ordering: Ordering::BigEndian,
            os: OperatingSystem::MacOS_Classic,
            sections: Some(sections),
            timestamp: u32_from_offset(contents, 16, Ordering::BigEndian),
            contents,
        })
    }

    pub fn compiled_date(&self) -> DateTime<Utc> {
        let janone1940 = DateTime::from_naive_utc_and_offset(
            NaiveDateTime::parse_from_str("1904-01-01 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap(),
            Utc,
        );
        janone1940 + Duration::seconds(self.timestamp as i64)
    }
}

impl<'a> ExecutableFile for Pef<'a> {
    fn type_name(&self) -> String {
        "PEF".into()
    }

    fn architecture(&self) -> &Architecture {
        &self.arch
    }

    fn pointer_size(&self) -> usize {
        32
    }

    fn operating_system(&self) -> &OperatingSystem {
        &self.os
    }
    fn compiled_timestamp(&self) -> Option<DateTime<Utc>> {
        Some(self.compiled_date())
    }

    fn num_sections(&self) -> u32 {
        if let Some(sections) = &self.sections {
            sections.0.len() as u32
        } else {
            0
        }
    }

    fn sections(&self) -> anyhow::Result<&Sections> {
        self.sections
            .as_deref()
            .ok_or_else(|| anyhow::Error::msg("No sections"))
            .context("")
    }
}

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

impl<'a> Display for Pef<'a> {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        writeln!(f, "PEF file:")?;
        writeln!(f, "\tOS: {}", self.os)?;
        writeln!(f, "\tArchitecture: {}", self.arch)?;
        writeln!(f, "\tOrdering: {}", self.ordering)?;
        if let Some(sections) = &self.sections {
            writeln!(f, "\t{} sections:", sections.0.len())?;
            for section in sections.0.iter() {
                writeln!(f, "\t\t{}", section)?;
            }
        }
        writeln!(
            f,
            "\tCompiled: {:?}",
            self.compiled_date().format("%Y-%m-%d %H:%M:%S").to_string()
        )?;
        writeln!(f, "\tSize: {}", self.contents.len())
    }
}

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

    #[test]
    fn beos() {
        const BYTES: &[u8] = include_bytes!("../../../testdata/pef/BeApp");

        let pef = Pef::from(BYTES).unwrap();
        eprintln!("BeOS:\n{pef}");
        assert_eq!(pef.arch, Architecture::PowerPC);
    }

    #[rstest]
    #[case(include_bytes!("../../../testdata/pef/MacOS_1"))]
    #[case(include_bytes!("../../../testdata/pef/MacOS_2"))]
    fn macos(#[case] bytes: &[u8]) {
        let pef = Pef::from(bytes).unwrap();
        eprintln!("Mac OS:\n{pef}");
        assert_eq!(pef.arch, Architecture::PowerPC);
    }
}
