// SPDX-License-Identifier: MIT OR Apache-2.0

//! Partition information protocol.

use crate::proto::unsafe_protocol;
use crate::{Char16, Guid, guid};

newtype_enum! {
    /// MBR OS type.
    ///
    /// Only two values are defined in the UEFI specification, other
    /// values are used by legacy operating systems.
    pub enum MbrOsType: u8 => {
        /// A fake partition covering the entire disk.
        GPT_PROTECTIVE = 0xee,

        /// UEFI system partition.
        UEFI_SYSTEM_PARTITION = 0xef,
    }
}

/// Legacy MBR Partition Record.
#[repr(C)]
#[repr(packed)]
#[derive(Clone, Copy, Debug)]
pub struct MbrPartitionRecord {
    /// If 0x80, this is the bootable legacy partition.
    pub boot_indicator: u8,

    /// Start of the partition in CHS address format.
    pub starting_chs: [u8; 3],

    /// Type of partition.
    pub os_type: MbrOsType,

    /// End of the partition in CHS address format.
    pub ending_chs: [u8; 3],

    /// Starting LBA of the partition on the disk.
    pub starting_lba: u32,

    /// Size of the partition in LBA units of logical blocks.
    pub size_in_lba: u32,
}

impl MbrPartitionRecord {
    /// True if the partition is a bootable legacy partition.
    #[must_use]
    pub const fn is_bootable(&self) -> bool {
        self.boot_indicator == 0x80
    }
}

newtype_enum! {
    /// GUID that defines the type of partition. Only three values are
    /// defined in the UEFI specification, OS vendors define their own
    /// Partition Type GUIDs.
    pub enum GptPartitionType: Guid => {
        /// Indicates a partition entry is unused.
        UNUSED_ENTRY = guid!("00000000-0000-0000-0000-000000000000"),

        /// EFI System Partition.
        EFI_SYSTEM_PARTITION = guid!("c12a7328-f81f-11d2-ba4b-00a0c93ec93b"),

        /// Partition containing a legacy MBR.
        LEGACY_MBR = guid!("024dee41-33e7-11d3-9d69-0008c781f39f"),
    }
}

bitflags::bitflags! {

    /// Attributes describing a GPT partition.
    ///
    /// * Bit 0: [`REQUIRED_PARTITION`][Self::REQUIRED_PARTITION]
    /// * Bit 1: [`NO_BLOCK_IO_PROTOCOL`][Self::NO_BLOCK_IO_PROTOCOL]
    /// * Bit 2: [`LEGACY_BIOS_BOOTABLE`][Self::LEGACY_BIOS_BOOTABLE]
    /// * Bits `3..=47`: reserved for future use and must be zero.
    /// * Bits `48..=63`: See
    /// [`type_specific_bits`][Self::type_specific_bits] and
    /// [`RESERVED_FOR_PARTITION_TYPE`][Self::RESERVED_FOR_PARTITION_TYPE].
    #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
    #[repr(transparent)]
    pub struct GptPartitionAttributes: u64 {
        /// Bit: Partition is required for the platform to function.
        const REQUIRED_PARTITION = 1;
        /// Bit: No [`BlockIO`] protocol will be created for this partition.
        ///
        /// [`BlockIO`]: uefi::proto::media::block::BlockIO
        const NO_BLOCK_IO_PROTOCOL = 1 << 1;

        /// Bit: Indicates that special software on a legacy BIOS system may
        /// treat this partition as bootable. UEFI boot managers must
        /// ignore the partition.
        const LEGACY_BIOS_BOOTABLE = 1 << 2;

        /// Mask for bits `48..=63`. The meaning of these bits depends
        /// on the partition type.
        const RESERVED_FOR_PARTITION_TYPE = 0xffff_0000_0000_0000;

        /// The meaning of this bit depends on the partition type.
        const TYPE_SPECIFIC_BIT_0 = 1 << 47;

        /// The meaning of this bit depends on the partition type.
        const TYPE_SPECIFIC_BIT_1 = 1 << 48;

        /// The meaning of this bit depends on the partition type.
        const TYPE_SPECIFIC_BIT_2 = 1 << 49;

        /// The meaning of this bit depends on the partition type.
        const TYPE_SPECIFIC_BIT_3 = 1 << 50;

        /// The meaning of this bit depends on the partition type.
        const TYPE_SPECIFIC_BIT_4 = 1 << 51;

        /// The meaning of this bit depends on the partition type.
        const TYPE_SPECIFIC_BIT_5 = 1 << 52;

        /// The meaning of this bit depends on the partition type.
        const TYPE_SPECIFIC_BIT_6 = 1 << 53;

        /// The meaning of this bit depends on the partition type.
        const TYPE_SPECIFIC_BIT_7 = 1 << 54;

        /// The meaning of this bit depends on the partition type.
        const TYPE_SPECIFIC_BIT_8 = 1 << 55;

        /// The meaning of this bit depends on the partition type.
        const TYPE_SPECIFIC_BIT_9 = 1 << 56;

        /// The meaning of this bit depends on the partition type.
        const TYPE_SPECIFIC_BIT_10 = 1 << 57;

        /// The meaning of this bit depends on the partition type.
        const TYPE_SPECIFIC_BIT_11 = 1 << 58;

        /// The meaning of this bit depends on the partition type.
        const TYPE_SPECIFIC_BIT_12 = 1 << 59;

        /// The meaning of this bit depends on the partition type.
        const TYPE_SPECIFIC_BIT_13 = 1 << 60;

        /// The meaning of this bit depends on the partition type.
        const TYPE_SPECIFIC_BIT_14 = 1 << 61;

        /// The meaning of this bit depends on the partition type.
        const TYPE_SPECIFIC_BIT_15 = 1 << 62;
    }
}

impl GptPartitionAttributes {
    /// Get bits `48..=63` as a [`u16`]. The meaning of these bits depends
    /// on the partition's type (see [`GptPartitionEntry::partition_type_guid`]).
    #[must_use]
    pub const fn type_specific_bits(&self) -> u16 {
        (self.0.bits() >> 48) as u16
    }
}

/// GPT/EFI Partition Entry.
#[repr(C)]
#[repr(packed)]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct GptPartitionEntry {
    /// GUID that defines the type of this Partition. A value of zero
    /// indicates that this partition entry is unused.
    pub partition_type_guid: GptPartitionType,

    /// GUID that is unique for every partition entry.
    pub unique_partition_guid: Guid,

    /// Starting LBA of the partition.
    pub starting_lba: u64,

    /// Ending LBA of the partition.
    pub ending_lba: u64,

    /// All attribute bits of the partition.
    pub attributes: GptPartitionAttributes,

    /// Null-terminated string containing a human-readable name of the
    /// partition.
    pub partition_name: [Char16; 36],
}

impl GptPartitionEntry {
    /// Get the number of blocks in the partition. Returns `None` if the
    /// end block is before the start block, or if the number doesn't
    /// fit in a `u64`.
    #[must_use]
    pub fn num_blocks(&self) -> Option<u64> {
        self.ending_lba
            .checked_sub(self.starting_lba)?
            .checked_add(1)
    }
}

newtype_enum! {
    /// Partition type.
    pub enum PartitionType: u32 => {
        /// Partition is not MBR or GPT.
        OTHER = 0x00,
        /// MBR partition.
        MBR = 0x01,
        /// GPT partition.
        GPT = 0x02,
    }
}

#[repr(C)]
#[derive(Clone, Copy)]
union PartitionInfoRecord {
    mbr: MbrPartitionRecord,
    gpt: GptPartitionEntry,
}

newtype_enum! {
    /// Partition info protocol revision.
    pub enum PartitionInfoRevision: u32 => {
        /// Revision of EFI_PARTITION_INFO_PROTOCOL_REVISION.
        PROTOCOL_REVISION = 0x0001000,
    }
}

/// Partition Info [`Protocol`].
///
/// Protocol for accessing partition information.
///
/// [`Protocol`]: uefi::proto::Protocol
#[allow(missing_debug_implementations)]
#[repr(C)]
#[repr(packed)]
#[unsafe_protocol("8cf2f62c-bc9b-4821-808d-ec9ec421a1a0")]
pub struct PartitionInfo {
    /// Revision of the partition info protocol.
    pub revision: PartitionInfoRevision,

    /// Type of partition.
    pub partition_type: PartitionType,

    system: u8,
    reserved: [u8; 7],
    record: PartitionInfoRecord,
}

impl PartitionInfo {
    /// True if the partition is an EFI system partition.
    #[must_use]
    pub const fn is_system(&self) -> bool {
        self.system == 1
    }

    /// Get the MBR partition record. Returns None if the partition
    /// type is not MBR.
    #[must_use]
    pub fn mbr_partition_record(&self) -> Option<&MbrPartitionRecord> {
        if { self.revision } != PartitionInfoRevision::PROTOCOL_REVISION {
            return None;
        }

        if { self.partition_type } == PartitionType::MBR {
            Some(unsafe { &self.record.mbr })
        } else {
            None
        }
    }

    /// Get the GPT partition entry. Returns None if the partition
    /// type is not GPT.
    #[must_use]
    pub fn gpt_partition_entry(&self) -> Option<&GptPartitionEntry> {
        if { self.revision } != PartitionInfoRevision::PROTOCOL_REVISION {
            return None;
        }

        if { self.partition_type } == PartitionType::GPT {
            Some(unsafe { &self.record.gpt })
        } else {
            None
        }
    }
}

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

    #[test]
    fn test_partition_attributes() {
        let attr: GptPartitionAttributes =
            GptPartitionAttributes::from_bits_retain(0xabcd_0000_0000_0007);
        assert_eq!(attr.type_specific_bits(), 0xabcd);
    }
}
