use crate::utils::u16_from_offset;
use crate::{Ordering, TypeMagic};

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

use anyhow::Result;
use uuid::{uuid, Uuid};

const DOCFILE_MAGIC: [u8; 8] = [0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1];

/// Clsid is the UUID which matches a Docfile subtype, and the first three segments
/// could be in big or little endian, so we have to check both
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Clsid {
    /// Little Endian representation, most common
    pub le_uuid: Uuid,

    /// Big Endian representation, unlikely to be encountered
    pub be_uuid: Uuid,
}

impl Clsid {
    pub const EXCEL5: Self = Clsid {
        le_uuid: uuid!("10080200-0000-0000-c000-000000000046"),
        be_uuid: uuid!("00020810-0000-0000-c000-000000000046"),
    };

    pub const EXCEL97: Self = Clsid {
        le_uuid: uuid!("20080200-0000-0000-c000-000000000046"),
        be_uuid: uuid!("00020820-0000-0000-c000-000000000046"),
    };

    pub const WORD6: Self = Clsid {
        le_uuid: uuid!("00090200-0000-0000-c000-000000000046"),
        be_uuid: uuid!("00020900-0000-0000-c000-000000000046"),
    };

    pub const DOC: Self = Clsid {
        le_uuid: uuid!("06090200-0000-0000-c000-000000000046"),
        be_uuid: uuid!("00020906-0000-0000-c000-000000000046"),
    };

    pub const POWERPOINT4: Self = Clsid {
        le_uuid: uuid!("51480400-0000-0000-c000-000000000046"),
        be_uuid: uuid!("00044851-0000-0000-c000-000000000046"),
    };

    pub const PPT: Self = Clsid {
        le_uuid: uuid!("108d8164-9b4f-cf11-86ea-00aa00b929e8"),
        be_uuid: uuid!("64818d10-4f9b-11cf-86ea-00aa00b929e8"),
    };
}

// http://fileformats.archiveteam.org/wiki/Microsoft_Compound_File
/// UUID file type, of which only a subset is of interest
/// This is how we can filter out container formats, like .msi (installer) files.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ClsidType {
    Excel,
    PowerPoint,
    Word,
    Unknown,
}

impl ClsidType {
    pub fn from(bytes: &[u8; 16]) -> Self {
        if Clsid::EXCEL5.le_uuid.as_bytes() == bytes
            || Clsid::EXCEL5.be_uuid.as_bytes() == bytes
            || Clsid::EXCEL97.le_uuid.as_bytes() == bytes
            || Clsid::EXCEL97.be_uuid.as_bytes() == bytes
        {
            return Self::Excel;
        }

        if Clsid::WORD6.le_uuid.as_bytes() == bytes
            || Clsid::WORD6.be_uuid.as_bytes() == bytes
            || Clsid::DOC.le_uuid.as_bytes() == bytes
            || Clsid::DOC.be_uuid.as_bytes() == bytes
        {
            return Self::Word;
        }

        if Clsid::PPT.le_uuid.as_bytes() == bytes
            || Clsid::PPT.be_uuid.as_bytes() == bytes
            || Clsid::POWERPOINT4.le_uuid.as_bytes() == bytes
            || Clsid::POWERPOINT4.be_uuid.as_bytes() == bytes
        {
            return Self::PowerPoint;
        }

        Self::Unknown
    }
}

impl Display for ClsidType {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        match self {
            ClsidType::Excel => write!(f, "Excel"),
            ClsidType::PowerPoint => write!(f, "PowerPoint"),
            ClsidType::Word => write!(f, "Word"),
            ClsidType::Unknown => write!(f, "Unknown/other"),
        }
    }
}

/// A struct representing the older Microsoft Office format, Office95, aka Docfile.
///
/// This format is really a container format, and could be used to hold a non-Office files,
/// such as installers (.msi files), Windows update files, and others. Here we are only concerned
/// with MS Office types.
#[derive(Clone, Debug)]
pub struct Office95<'a> {
    /// Sub-type for the file
    pub clsid: ClsidType,

    /// Minor version of the format
    pub minor_version: u16,

    /// Major version of the format, also called `DllVersion`
    pub major_version: u16,

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

impl<'a> Office95<'a> {
    pub fn from(contents: &'a [u8]) -> Result<Self> {
        let offset: [u8; 4] = contents[48..52].try_into().unwrap();
        let offset_int = u32::from_le_bytes(offset);
        let offset_int = (512 * (1 + offset_int) + 80) as usize;
        let clsid: [u8; 16] = contents[offset_int..offset_int + 16].try_into().unwrap();
        let minor_version = u16_from_offset(contents, offset_int + 16, Ordering::LittleEndian);
        let major_version = u16_from_offset(contents, offset_int + 18, Ordering::LittleEndian);

        Ok(Self {
            clsid: ClsidType::from(&clsid),
            minor_version,
            major_version,
            contents,
        })
    }
}

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

impl<'a> Display for Office95<'a> {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        write!(f, "Type: {}", self.clsid)?;
        write!(f, "Size: {}", self.contents.len())
    }
}

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

    #[test]
    fn word() {
        const BYTES: &[u8] = include_bytes!("../../testdata/office95/word.doc");
        let office = Office95::from(BYTES).unwrap();
        println!(
            "Word, version {}.{}",
            office.major_version, office.minor_version
        );
        assert_eq!(office.clsid, ClsidType::Word);
    }

    #[test]
    fn excel() {
        const BYTES: &[u8] = include_bytes!("../../testdata/office95/excel.xls");
        let office = Office95::from(BYTES).unwrap();
        println!(
            "Excel, version {}.{}",
            office.major_version, office.minor_version
        );
        assert_eq!(office.clsid, ClsidType::Excel);
    }

    #[test]
    fn powerpoint() {
        const BYTES: &[u8] = include_bytes!("../../testdata/office95/powerpoint.ppt");
        let office = Office95::from(BYTES).unwrap();
        println!(
            "PowerPoint, version {}.{}",
            office.major_version, office.minor_version
        );
        assert_eq!(office.clsid, ClsidType::PowerPoint);
    }
}
