# Copyright (C) 2026, Advanced Micro Devices, Inc.  All rights reserved.
# Portions of this file consist of AI-generated content.
# SPDX-License-Identifier: MIT

"""Versal boot image loader module.

Reference: https://docs.amd.com/r/en-US/ug1283-bootgen-user-guide/Versal-Adaptive-SoC-Boot-Image-Format
"""

from typing import Optional, List, Tuple
from enum import IntEnum
import struct
from .loader import (
    BootImageLoader,
    BootHeader,
    ImageHeaderTable,
    ImageHeader,
    ImageHeaderWrapper,
    PartitionHeader,
)


class VersalOptionalData:
    """Represents an optional data entry in the Versal PDI.
    
    Optional data is located immediately after the Image Header Table (IHT).
    Each entry contains metadata such as PLM build configuration, version info, etc.
    
    Reference: https://docs.amd.com/r/en-US/ug1283-bootgen-user-guide/Optional-Data
    """
    
    class DataID(IntEnum):
        """Optional data ID types."""
        NONE = 0  # None, can be used for padding
        PLM_BUILD_TIME_CONFIG = 1  # PLM Build Time configuration metadata
        VERSION_INFO = 2  # Data structure version information for In-Place PLM Update
        RESERVED = 4  # Reserved
    
    def __init__(self):
        """Initialize the optional data entry."""
        self.data_id: Optional[int] = None  # The data ID (2 bytes)
        self.size_words: Optional[int] = None  # Total size in words (2 bytes)
        self.data: Optional[bytes] = None  # Actual data
        self.checksum: Optional[int] = None  # Checksum (last word)
    
    def get_data_id_name(self) -> str:
        """Get the name of the data ID type.
        
        Returns:
            A string describing the data ID type.
        """
        if self.data_id is None:
            return "UNKNOWN"
        
        # DataID values > 0x20 are user-defined
        if self.data_id > 0x20:
            return f"USER_{self.data_id:02X}"
        
        try:
            return VersalOptionalData.DataID(self.data_id).name
        except ValueError:
            return f"UNKNOWN_{self.data_id:02X}"
    
    def verify_checksum(self) -> bool:
        """Verify the checksum for this optional data entry.
        
        The checksum is the inverted sum (one's complement) of all previous 
        words in the data structure.
        
        Returns:
            True if checksum is valid, False otherwise.
        """
        if self.checksum is None or self.data_id is None or self.size_words is None:
            return False
        
        # Start with ID and size (packed as a single 32-bit word)
        words = [(self.data_id & 0xFFFF) | ((self.size_words & 0xFFFF) << 16)]
        
        # Add data words (excluding the checksum itself)
        if self.data:
            for i in range(0, len(self.data), 4):
                if i + 4 <= len(self.data):
                    word = struct.unpack('<I', self.data[i:i+4])[0]
                    words.append(word)
        
        # Calculate checksum: sum all words and invert (one's complement)
        computed = (~sum(words)) & 0xFFFFFFFF
        
        return computed == self.checksum


class VersalBootHeader(BootHeader):
    """Versal-specific boot header with additional fields.
    
    The Versal boot header (PDI header) is used by PMC BootROM.
    Based on the attributes set in the boot header, PMC BootROM validates
    the Platform Loader and Manager (PLM) and loads it to the PPU RAM.
    """
    
    # SelectMAP bus width patterns (first 16 bytes)
    SMAP_WIDTH_X8 = bytes([0x00, 0x00, 0x00, 0xDD, 0x11, 0x22, 0x33, 0x44,
                           0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC])
    SMAP_WIDTH_X16 = bytes([0x00, 0x00, 0xDD, 0x00, 0x22, 0x11, 0x44, 0x33,
                            0x66, 0x55, 0x88, 0x77, 0xAA, 0x99, 0xCC, 0xBB])
    SMAP_WIDTH_X32 = bytes([0xDD, 0x00, 0x00, 0x00, 0x44, 0x33, 0x22, 0x11,
                            0x88, 0x77, 0x66, 0x55, 0xCC, 0xBB, 0xAA, 0x99])
    
    # QSPI bus width word (0xAA995566 little-endian)
    QSPI_BUS_WIDTH = 0xAA995566
    
    # Image identification string "XLNX" (0x584C4E58 little-endian)
    IMAGE_IDENTIFICATION = 0x584C4E58
    
    # Encryption key source values
    KEY_SOURCE_UNENCRYPTED = 0x00000000
    KEY_SOURCE_EFUSE_RED = 0xA5C3C5A3
    KEY_SOURCE_EFUSE_BLACK = 0xA5C3C5A5
    KEY_SOURCE_BBRAM_RED = 0x3A5C3C5A
    KEY_SOURCE_BBRAM_BLACK = 0x3A5C3C59
    KEY_SOURCE_BH_BLACK = 0xA35C7C53
    
    # Boot header offsets
    BH_SMAP_BUS_WIDTH_OFFSET = 0x00  # 16 bytes
    BH_QSPI_BUS_WIDTH_OFFSET = 0x10
    BH_IMAGE_ID_OFFSET = 0x14
    BH_KEY_SOURCE_OFFSET = 0x18
    BH_PLM_SOURCE_OFFSET = 0x1C
    BH_PMC_DATA_LOAD_ADDR_OFFSET = 0x20
    BH_PMC_DATA_LENGTH_OFFSET = 0x24
    BH_TOTAL_PMC_DATA_LENGTH_OFFSET = 0x28
    BH_PLM_LENGTH_OFFSET = 0x2C
    BH_TOTAL_PLM_LENGTH_OFFSET = 0x30
    BH_ATTRIBUTES_OFFSET = 0x34
    BH_BLACK_KEY_OFFSET = 0x38  # 32 bytes
    BH_BLACK_IV_OFFSET = 0x58  # 12 bytes
    BH_SECURE_HDR_IV_OFFSET = 0x64  # 12 bytes
    BH_PUF_SHUTTER_VALUE_OFFSET = 0x70
    BH_SECURE_HDR_IV_PMC_DATA_OFFSET = 0x74  # 12 bytes
    BH_RESERVED1_OFFSET = 0x80  # 68 bytes
    BH_META_HEADER_OFFSET = 0xC4
    BH_RESERVED2_OFFSET = 0xC8  # 96 bytes (0xC8 - 0x127)
    BH_REGISTER_INIT_OFFSET = 0x128  # 2048 bytes
    BH_PUF_HELPER_DATA_OFFSET = 0x928  # 1544 bytes
    BH_CHECKSUM_OFFSET = 0xF30
    BH_SHA3_PADDING_OFFSET = 0xF34  # 76 bytes
    
    # Boot header total size
    BOOT_HEADER_SIZE = 0xF80  # 3968 bytes
    
    # Boot header attribute bit shifts
    BH_ATTR_PUF_MODE_SHIFT = 16
    BH_ATTR_BH_AUTH_SHIFT = 14
    BH_ATTR_DPA_CM_SHIFT = 10
    BH_ATTR_CHECKSUM_SHIFT = 8
    BH_ATTR_PUF_HD_SHIFT = 6
    
    # Boot header attribute bit masks
    BH_ATTR_PUF_MODE_MASK = 0x3
    BH_ATTR_BH_AUTH_MASK = 0x3
    BH_ATTR_DPA_CM_MASK = 0x3
    BH_ATTR_CHECKSUM_MASK = 0x3
    BH_ATTR_PUF_HD_MASK = 0x3

    class PufMode(IntEnum):
        """PUF Mode types (bits 17:16)."""
        DISABLED = 0
        RESERVED_1 = 1
        RESERVED_2 = 2
        PUF_4K = 3

    class BHAuthentication(IntEnum):
        """Boot Header Authentication types (bits 15:14)."""
        EFUSE_0 = 0
        EFUSE_1 = 1
        EFUSE_2 = 2
        BH_AUTH = 3  # Authentication excluding PPK hash and SPK ID verification

    class ChecksumType(IntEnum):
        """Checksum types (bits 9:8)."""
        RESERVED_0 = 0
        RESERVED_1 = 1
        RESERVED_2 = 2
        SHA3 = 3

    class PufHdLocation(IntEnum):
        """PUF HD Location types (bits 7:6)."""
        EFUSE_0 = 0
        EFUSE_1 = 1
        EFUSE_2 = 2
        BOOT_HEADER = 3

    class DpaCm(IntEnum):
        """DPA Counter Measure types (bits 11:10)."""
        DISABLED_0 = 0
        DISABLED_1 = 1
        DISABLED_2 = 2
        ENABLED = 3
    
    def __init__(self):
        """Initialize the Versal boot header."""
        super().__init__()
        self.smap_bus_width: Optional[bytes] = None  # 0x00 - 0x0F (16 bytes)
        self.qspi_bus_width: Optional[int] = None  # 0x10
        self.image_identification: Optional[int] = None  # 0x14
        self.encryption_key_source: Optional[int] = None  # 0x18
        self.plm_source_offset: Optional[int] = None  # 0x1C
        self.pmc_data_load_address: Optional[int] = None  # 0x20
        self.pmc_data_length: Optional[int] = None  # 0x24
        self.total_pmc_data_length: Optional[int] = None  # 0x28
        self.plm_length: Optional[int] = None  # 0x2C
        self.total_plm_length: Optional[int] = None  # 0x30
        self.boot_header_attributes: Optional[int] = None  # 0x34
        self.black_key: Optional[bytes] = None  # 0x38-0x57 (32 bytes)
        self.black_iv: Optional[bytes] = None  # 0x58-0x63 (12 bytes)
        self.secure_header_iv: Optional[bytes] = None  # 0x64-0x6F (12 bytes)
        self.puf_shutter_value: Optional[int] = None  # 0x70
        self.secure_header_iv_pmc_data: Optional[bytes] = None  # 0x74-0x7F (12 bytes)
        # Reserved 0x80-0xC3 (68 bytes)
        self.meta_header_offset: Optional[int] = None  # 0xC4
        # Reserved 0xC8-0x127 (96 bytes)
        self.register_init: Optional[bytes] = None  # 0x128-0x927 (2048 bytes)
        self.puf_helper_data: Optional[bytes] = None  # 0x928-0xF2F (1544 bytes)
        self.boot_header_checksum: Optional[int] = None  # 0xF30
        self.sha3_padding: Optional[bytes] = None  # 0xF34-0xF7F (76 bytes)

    def compute_checksum(self) -> int:
        """Compute the checksum over the boot header fields.
        
        The checksum is computed over all words from 0x10 to 0xF2C inclusive.
        (Excludes SMAP bus width detection and the checksum/padding at the end)
        
        Returns:
            The computed checksum as a 32-bit unsigned integer.
        """
        words = [
            self.qspi_bus_width or 0,  # 0x10
            self.image_identification or 0,  # 0x14
            self.encryption_key_source or 0,  # 0x18
            self.plm_source_offset or 0,  # 0x1C
            self.pmc_data_load_address or 0,  # 0x20
            self.pmc_data_length or 0,  # 0x24
            self.total_pmc_data_length or 0,  # 0x28
            self.plm_length or 0,  # 0x2C
            self.total_plm_length or 0,  # 0x30
            self.boot_header_attributes or 0,  # 0x34
        ]
        
        # Add black key (32 bytes = 8 words)
        if self.black_key:
            for i in range(0, 32, 4):
                words.append(int.from_bytes(self.black_key[i:i+4], 'little'))
        else:
            words.extend([0] * 8)
        
        # Add black IV (12 bytes = 3 words)
        if self.black_iv:
            for i in range(0, 12, 4):
                words.append(int.from_bytes(self.black_iv[i:i+4], 'little'))
        else:
            words.extend([0] * 3)
        
        # Add secure header IV (12 bytes = 3 words)
        if self.secure_header_iv:
            for i in range(0, 12, 4):
                words.append(int.from_bytes(self.secure_header_iv[i:i+4], 'little'))
        else:
            words.extend([0] * 3)
        
        # Add PUF shutter value
        words.append(self.puf_shutter_value or 0)  # 0x70
        
        # Add secure header IV for PMC data (12 bytes = 3 words)
        if self.secure_header_iv_pmc_data:
            for i in range(0, 12, 4):
                words.append(int.from_bytes(self.secure_header_iv_pmc_data[i:i+4], 'little'))
        else:
            words.extend([0] * 3)
        
        # Add reserved area 0x80-0xC3 (68 bytes = 17 words)
        words.extend([0] * 17)
        
        # Add meta header offset
        words.append(self.meta_header_offset or 0)  # 0xC4
        
        # Add reserved area 0xC8-0x127 (96 bytes = 24 words)
        words.extend([0] * 24)
        
        # Add register init (2048 bytes = 512 words)
        if self.register_init:
            for i in range(0, 2048, 4):
                if i + 4 <= len(self.register_init):
                    words.append(int.from_bytes(self.register_init[i:i+4], 'little'))
                else:
                    words.append(0)
        else:
            words.extend([0] * 512)
        
        # Add PUF helper data (1544 bytes = 386 words)
        if self.puf_helper_data:
            for i in range(0, 1544, 4):
                if i + 4 <= len(self.puf_helper_data):
                    words.append(int.from_bytes(self.puf_helper_data[i:i+4], 'little'))
                else:
                    words.append(0)
        else:
            words.extend([0] * 386)
        
        # Sum all words and mask to 32 bits
        checksum = sum(words) & 0xFFFFFFFF
        
        # Invert (one's complement)
        checksum = (~checksum) & 0xFFFFFFFF
        
        return checksum

    def verify_checksum(self) -> bool:
        """Verify the boot header checksum.
        
        Returns:
            True if the checksum is valid, False otherwise.
        """
        if self.boot_header_checksum is None:
            return False
        return self.compute_checksum() == self.boot_header_checksum

    def get_puf_mode(self) -> 'VersalBootHeader.PufMode':
        """Extract PUF mode from boot header attributes (bits 17:16).
        
        Returns:
            PufMode enum value.
        """
        if self.boot_header_attributes is None:
            return VersalBootHeader.PufMode.DISABLED
        return VersalBootHeader.PufMode((self.boot_header_attributes >> 16) & 0x3)

    def get_bh_authentication(self) -> 'VersalBootHeader.BHAuthentication':
        """Extract boot header authentication from attributes (bits 15:14).
        
        Returns:
            BHAuthentication enum value.
        """
        if self.boot_header_attributes is None:
            return VersalBootHeader.BHAuthentication.EFUSE_0
        return VersalBootHeader.BHAuthentication((self.boot_header_attributes >> 14) & 0x3)

    def get_dpa_cm(self) -> 'VersalBootHeader.DpaCm':
        """Extract DPA counter measure from attributes (bits 11:10).
        
        Returns:
            DpaCm enum value.
        """
        if self.boot_header_attributes is None:
            return VersalBootHeader.DpaCm.DISABLED_0
        return VersalBootHeader.DpaCm((self.boot_header_attributes >> 10) & 0x3)

    def get_checksum_type(self) -> 'VersalBootHeader.ChecksumType':
        """Extract checksum type from boot header attributes (bits 9:8).
        
        Returns:
            ChecksumType enum value.
        """
        if self.boot_header_attributes is None:
            return VersalBootHeader.ChecksumType.RESERVED_0
        return VersalBootHeader.ChecksumType((self.boot_header_attributes >> 8) & 0x3)

    def get_puf_hd_location(self) -> 'VersalBootHeader.PufHdLocation':
        """Extract PUF HD location from boot header attributes (bits 7:6).
        
        Returns:
            PufHdLocation enum value.
        """
        if self.boot_header_attributes is None:
            return VersalBootHeader.PufHdLocation.EFUSE_0
        return VersalBootHeader.PufHdLocation((self.boot_header_attributes >> 6) & 0x3)

    def get_attributes_summary(self) -> str:
        """Get a human-readable summary of the boot header attributes.
        
        Returns:
            String describing the boot header attributes.
        """
        return (f"PUF_Mode={self.get_puf_mode().name}, "
                f"BH_Auth={self.get_bh_authentication().name}, "
                f"DPA_CM={self.get_dpa_cm().name}, "
                f"Checksum={self.get_checksum_type().name}, "
                f"PUF_HD={self.get_puf_hd_location().name}")

    def to_bytes(self, include_smap_width: bool = False) -> bytes:
        """Serialize the boot header to bytes.
        
        This method generates the binary representation of the boot header
        suitable for writing to a boot_bh.bin file. By default, it excludes
        the first 16 bytes (SMAP bus width detection words) as these are not
        required by the BootROM/PLM.
        
        The output format matches bootgen's dump format for boot headers.
        
        Args:
            include_smap_width: If True, include the 16-byte SMAP bus width
                               detection words at the beginning. Default is False.
        
        Returns:
            The boot header as bytes, starting from offset 0x10 (or 0x00 if
            include_smap_width is True).
        """
        data = bytearray()
        
        # Optionally include SMAP bus width (0x00-0x0F, 16 bytes)
        if include_smap_width:
            if self.smap_bus_width:
                data.extend(self.smap_bus_width)
            else:
                data.extend(bytes(16))
        
        # QSPI bus width / Width Detection Word (0x10, 4 bytes)
        data.extend(struct.pack('<I', self.qspi_bus_width or self.QSPI_BUS_WIDTH))
        
        # Image Identification (0x14, 4 bytes)
        data.extend(struct.pack('<I', self.image_identification or self.IMAGE_IDENTIFICATION))
        
        # Encryption Key Source (0x18, 4 bytes)
        data.extend(struct.pack('<I', self.encryption_key_source or self.KEY_SOURCE_UNENCRYPTED))
        
        # PLM Source Offset (0x1C, 4 bytes)
        data.extend(struct.pack('<I', self.plm_source_offset or 0))
        
        # PMC Data Load Address (0x20, 4 bytes)
        data.extend(struct.pack('<I', self.pmc_data_load_address or 0))
        
        # PMC Data Length (0x24, 4 bytes)
        data.extend(struct.pack('<I', self.pmc_data_length or 0))
        
        # Total PMC Data Length (0x28, 4 bytes)
        data.extend(struct.pack('<I', self.total_pmc_data_length or 0))
        
        # PLM Length (0x2C, 4 bytes)
        data.extend(struct.pack('<I', self.plm_length or 0))
        
        # Total PLM Length (0x30, 4 bytes)
        data.extend(struct.pack('<I', self.total_plm_length or 0))
        
        # Boot Header Attributes (0x34, 4 bytes)
        data.extend(struct.pack('<I', self.boot_header_attributes or 0))
        
        # Black Key (0x38-0x57, 32 bytes)
        if self.black_key:
            data.extend(self.black_key)
        else:
            data.extend(bytes(32))
        
        # Black IV (0x58-0x63, 12 bytes)
        if self.black_iv:
            data.extend(self.black_iv)
        else:
            data.extend(bytes(12))
        
        # Secure Header IV (0x64-0x6F, 12 bytes)
        if self.secure_header_iv:
            data.extend(self.secure_header_iv)
        else:
            data.extend(bytes(12))
        
        # PUF Shutter Value (0x70, 4 bytes)
        data.extend(struct.pack('<I', self.puf_shutter_value or 0))
        
        # Secure Header IV for PMC Data (0x74-0x7F, 12 bytes)
        if self.secure_header_iv_pmc_data:
            data.extend(self.secure_header_iv_pmc_data)
        else:
            data.extend(bytes(12))
        
        # Reserved (0x80-0xC3, 68 bytes)
        data.extend(bytes(68))
        
        # Meta Header Offset (0xC4, 4 bytes)
        data.extend(struct.pack('<I', self.meta_header_offset or 0))
        
        # Reserved (0xC8-0x127, 96 bytes)
        data.extend(bytes(96))
        
        # Register Init (0x128-0x927, 2048 bytes)
        if self.register_init:
            data.extend(self.register_init[:2048])
            if len(self.register_init) < 2048:
                data.extend(bytes(2048 - len(self.register_init)))
        else:
            data.extend(bytes(2048))
        
        # PUF Helper Data (0x928-0xF2F, 1544 bytes)
        if self.puf_helper_data:
            data.extend(self.puf_helper_data[:1544])
            if len(self.puf_helper_data) < 1544:
                data.extend(bytes(1544 - len(self.puf_helper_data)))
        else:
            data.extend(bytes(1544))
        
        # Boot Header Checksum (0xF30, 4 bytes)
        data.extend(struct.pack('<I', self.boot_header_checksum or 0))
        
        # SHA3 Padding (0xF34-0xF7F, 76 bytes)
        if self.sha3_padding:
            data.extend(self.sha3_padding[:76])
            if len(self.sha3_padding) < 76:
                data.extend(bytes(76 - len(self.sha3_padding)))
        else:
            data.extend(bytes(76))
        
        return bytes(data)


class VersalImageHeaderTable(ImageHeaderTable):
    """Versal-specific image header table."""
    
    # Valid versions
    VERSION_2_0 = 0x00020000  # IHT, PHT sizes doubled
    VERSION_3_0 = 0x00030000  # updated secure chunk size to 32 KB from 64 KB
    VERSION_4_0 = 0x00040000  # Added AAD support for IHT
    
    # Image Header Table offsets
    IHT_VERSION_OFFSET = 0x00
    IHT_IMAGE_COUNT_OFFSET = 0x04
    IHT_IMAGE_HEADER_OFFSET = 0x08
    IHT_PARTITION_COUNT_OFFSET = 0x0C
    IHT_PARTITION_HEADER_OFFSET = 0x10
    IHT_SECONDARY_BOOT_ADDR_OFFSET = 0x14
    IHT_RESERVED1_OFFSET = 0x18
    IHT_ATTRIBUTES_OFFSET = 0x1C
    IHT_PDI_ID_OFFSET = 0x20
    IHT_PARENT_ID_OFFSET = 0x24
    IHT_IDENTIFICATION_STRING_OFFSET = 0x28
    IHT_HEADERS_SIZE_OFFSET = 0x2C
    IHT_TOTAL_META_HEADER_LENGTH_OFFSET = 0x30
    IHT_META_HEADER_IV_OFFSET = 0x34  # 12 bytes
    IHT_ENCRYPTION_STATUS_OFFSET = 0x40
    IHT_RESERVED2_OFFSET = 0x44
    IHT_META_HEADER_AC_OFFSET = 0x48
    IHT_META_HEADER_BLACK_IV_OFFSET = 0x4C  # 12 bytes
    IHT_OPTIONAL_DATA_LENGTH_OFFSET = 0x58
    IHT_RESERVED3_OFFSET = 0x5C  # 32 bytes (0x5C - 0x7B)
    IHT_CHECKSUM_OFFSET = 0x7C
    
    # Image Header Table size
    IMAGE_HEADER_TABLE_SIZE = 128  # 0x80 bytes
    
    # PDI identification strings
    PDI_FULL = 0x49445046  # "FPDI" - Full PDI with boot header
    PDI_PARTIAL = 0x49445050  # "PPDI" - Partial/Sub-system PDI
    
    # IHT attribute bit shifts
    IHT_ATTR_PUF_HD_LOCATION_SHIFT = 14
    IHT_ATTR_DPACM_ENABLE_SHIFT = 12
    IHT_ATTR_SECONDARY_BOOT_DEVICE_SHIFT = 6
    
    # IHT attribute bit masks
    IHT_ATTR_PUF_HD_LOCATION_MASK = 0x1
    IHT_ATTR_DPACM_ENABLE_MASK = 0x1
    IHT_ATTR_SECONDARY_BOOT_DEVICE_MASK = 0x3F

    class SecondaryBootDevice(IntEnum):
        """Secondary boot device types (bits 11:6)."""
        SAME_BOOT_DEVICE = 0
        QSPI32 = 1
        QSPI24 = 2
        RESERVED_3 = 3
        SD0 = 4
        SD1 = 5
        SDLS = 6
        EMMC_MMC = 7
        USB = 8
        RESERVED_9 = 9
        PCIE = 10
        RESERVED_11 = 11
        OSPI = 12
        SMAP = 13
        SBI = 14
        SD0RAW = 15
        SD1RAW = 16
        SDLSRAW = 17
        MMCRAW = 18
        MMC0 = 19
        MMC0RAW = 20
        IMAGESTORE = 21
    
    def __init__(self):
        """Initialize the Versal image header table."""
        super().__init__()
        self.version: Optional[int] = None  # 0x00
        self.total_image_count: Optional[int] = None  # 0x04
        self.image_header_offset: Optional[int] = None  # 0x08 (word offset)
        self.total_partition_count: Optional[int] = None  # 0x0C
        self.partition_header_offset: Optional[int] = None  # 0x10 (word offset)
        self.secondary_boot_device_address: Optional[int] = None  # 0x14
        # Reserved 0x18
        self.attributes: Optional[int] = None  # 0x1C
        self.pdi_id: Optional[int] = None  # 0x20
        self.parent_id: Optional[int] = None  # 0x24
        self.identification_string: Optional[int] = None  # 0x28 ("FPDI" or "PPDI")
        self.headers_size: Optional[int] = None  # 0x2C (packed sizes)
        self.total_meta_header_length: Optional[int] = None  # 0x30 (words)
        self.meta_header_iv: Optional[bytes] = None  # 0x34-0x3F (12 bytes)
        self.encryption_status: Optional[int] = None  # 0x40
        # Reserved 0x44
        self.meta_header_ac_offset: Optional[int] = None  # 0x48 (word offset)
        self.meta_header_black_iv: Optional[bytes] = None  # 0x4C-0x57 (12 bytes)
        self.optional_data_length: Optional[int] = None  # 0x58 (words)
        # Reserved 0x5C-0x7B
        self.checksum: Optional[int] = None  # 0x7C

    def get_iht_size(self) -> int:
        """Get Image Header Table size in words from headers_size field (bits 7:0).
        
        Returns:
            IHT size in words.
        """
        if self.headers_size is None:
            return 0
        return self.headers_size & 0xFF

    def get_ih_size(self) -> int:
        """Get Image Header size in words from headers_size field (bits 15:8).
        
        Returns:
            Image Header size in words.
        """
        if self.headers_size is None:
            return 0
        return (self.headers_size >> 8) & 0xFF

    def get_ph_size(self) -> int:
        """Get Partition Header size in words from headers_size field (bits 23:16).
        
        Returns:
            Partition Header size in words.
        """
        if self.headers_size is None:
            return 0
        return (self.headers_size >> 16) & 0xFF

    def get_secondary_boot_device(self) -> 'VersalImageHeaderTable.SecondaryBootDevice':
        """Extract secondary boot device from attributes (bits 11:6).
        
        Returns:
            SecondaryBootDevice enum value.
        """
        if self.attributes is None:
            return VersalImageHeaderTable.SecondaryBootDevice.SAME_BOOT_DEVICE
        device_val = (self.attributes >> 6) & 0x3F
        try:
            return VersalImageHeaderTable.SecondaryBootDevice(device_val)
        except ValueError:
            return VersalImageHeaderTable.SecondaryBootDevice.SAME_BOOT_DEVICE

    def get_dpacm_enabled(self) -> bool:
        """Check if DPA counter measure is enabled (bit 12).
        
        Returns:
            True if DPACM is enabled, False otherwise.
        """
        if self.attributes is None:
            return False
        return bool((self.attributes >> 12) & 0x1)

    def get_puf_hd_in_bh(self) -> bool:
        """Check if PUF helper data is in boot header (bit 14).
        
        Returns:
            True if PUF HD is in boot header, False if in eFuse.
        """
        if self.attributes is None:
            return False
        return bool((self.attributes >> 14) & 0x1)

    def is_version_valid(self) -> bool:
        """Check if the version is valid.
        
        Returns:
            True if version is a known valid version, False otherwise.
        """
        return self.version in (self.VERSION_2_0, self.VERSION_3_0, self.VERSION_4_0)

    def is_full_pdi(self) -> bool:
        """Check if this is a full PDI (with boot header).
        
        Returns:
            True if identification string is "FPDI", False otherwise.
        """
        return self.identification_string == self.PDI_FULL

    def is_partial_pdi(self) -> bool:
        """Check if this is a partial PDI (sub-system PDI).
        
        Returns:
            True if identification string is "PPDI", False otherwise.
        """
        return self.identification_string == self.PDI_PARTIAL


class VersalImageHeader(ImageHeader):
    """Versal-specific image header."""
    
    # Image Header offsets
    IH_FIRST_PARTITION_HEADER_OFFSET = 0x00
    IH_PARTITION_COUNT_OFFSET = 0x04
    IH_REVOKE_ID_OFFSET = 0x08
    IH_ATTRIBUTES_OFFSET = 0x0C
    IH_IMAGE_NAME_OFFSET = 0x10  # 16 bytes
    IH_IMAGE_NODE_ID_OFFSET = 0x20
    IH_UNIQUE_ID_OFFSET = 0x24
    IH_PARENT_UNIQUE_ID_OFFSET = 0x28
    IH_FUNCTION_ID_OFFSET = 0x2C
    IH_DDR_LOW_ADDR_OFFSET = 0x30
    IH_DDR_HIGH_ADDR_OFFSET = 0x34
    IH_RESERVED_OFFSET = 0x38
    IH_CHECKSUM_OFFSET = 0x3C
    
    # Image Header size
    IMAGE_HEADER_SIZE = 64  # 0x40 bytes
    
    # Image attribute bit shifts
    IH_ATTR_DELAY_HANDOFF_SHIFT = 8
    IH_ATTR_DELAY_LOAD_SHIFT = 7
    IH_ATTR_COPY_TO_MEMORY_SHIFT = 6
    IH_ATTR_IMAGE_OWNER_SHIFT = 3
    
    # Image attribute bit masks
    IH_ATTR_DELAY_HANDOFF_MASK = 0x1
    IH_ATTR_DELAY_LOAD_MASK = 0x1
    IH_ATTR_COPY_TO_MEMORY_MASK = 0x1
    IH_ATTR_IMAGE_OWNER_MASK = 0x7

    class ImageOwner(IntEnum):
        """Image owner types (bits 5:3)."""
        PLM = 0
        NON_PLM = 1
        RESERVED_2 = 2
        RESERVED_3 = 3
        RESERVED_4 = 4
        RESERVED_5 = 5
        RESERVED_6 = 6
        RESERVED_7 = 7
    
    def __init__(self):
        """Initialize the Versal image header."""
        super().__init__()
        self.first_partition_header_offset: Optional[int] = None  # 0x00 (word offset)
        self.partition_count: Optional[int] = None  # 0x04
        self.revoke_id: Optional[int] = None  # 0x08
        self.attributes: Optional[int] = None  # 0x0C
        self.image_name: Optional[str] = None  # 0x10-0x1F (16 bytes, null-terminated)
        self.image_node_id: Optional[int] = None  # 0x20
        self.unique_id: Optional[int] = None  # 0x24
        self.parent_unique_id: Optional[int] = None  # 0x28
        self.function_id: Optional[int] = None  # 0x2C
        self.ddr_low_address: Optional[int] = None  # 0x30
        self.ddr_high_address: Optional[int] = None  # 0x34
        # Reserved 0x38
        self.checksum: Optional[int] = None  # 0x3C

    def get_image_owner(self) -> 'VersalImageHeader.ImageOwner':
        """Extract image owner from attributes (bits 5:3).
        
        Returns:
            ImageOwner enum value.
        """
        if self.attributes is None:
            return VersalImageHeader.ImageOwner.PLM
        return VersalImageHeader.ImageOwner((self.attributes >> 3) & 0x7)

    def get_copy_to_memory(self) -> bool:
        """Check if image should be copied to memory (bit 6).
        
        Returns:
            True if copy to memory is enabled, False otherwise.
        """
        if self.attributes is None:
            return False
        return bool((self.attributes >> 6) & 0x1)

    def get_delay_load(self) -> bool:
        """Check if image load should be delayed (bit 7).
        
        Returns:
            True if delay load is enabled, False otherwise.
        """
        if self.attributes is None:
            return False
        return bool((self.attributes >> 7) & 0x1)

    def get_delay_handoff(self) -> bool:
        """Check if image handoff should be delayed (bit 8).
        
        Returns:
            True if delay handoff is enabled, False otherwise.
        """
        if self.attributes is None:
            return False
        return bool((self.attributes >> 8) & 0x1)

    def get_ddr_address(self) -> int:
        """Get the full 64-bit DDR address for memory copy.
        
        Returns:
            The 64-bit DDR address.
        """
        lo = self.ddr_low_address or 0
        hi = self.ddr_high_address or 0
        return (hi << 32) | lo


class VersalPartitionHeader(PartitionHeader):
    """Versal-specific partition header."""
    
    # Maximum partitions
    MAX_NUM_PARTITIONS = 32
    
    # Partition header offsets
    PH_ENCRYPTED_LENGTH_OFFSET = 0x00
    PH_UNENCRYPTED_LENGTH_OFFSET = 0x04
    PH_TOTAL_LENGTH_OFFSET = 0x08
    PH_NEXT_PARTITION_OFFSET = 0x0C
    PH_EXEC_ADDR_LO_OFFSET = 0x10
    PH_EXEC_ADDR_HI_OFFSET = 0x14
    PH_LOAD_ADDR_LO_OFFSET = 0x18
    PH_LOAD_ADDR_HI_OFFSET = 0x1C
    PH_DATA_OFFSET = 0x20
    PH_ATTRIBUTES_OFFSET = 0x24
    PH_SECTION_COUNT_OFFSET = 0x28
    PH_CHECKSUM_WORD_OFFSET = 0x2C
    PH_PARTITION_ID_OFFSET = 0x30
    PH_AC_OFFSET = 0x34
    PH_IV_OFFSET = 0x38  # 12 bytes
    PH_ENCRYPTION_KEY_SELECT_OFFSET = 0x44
    PH_BLACK_IV_OFFSET = 0x48  # 12 bytes
    PH_REVOKE_ID_OFFSET = 0x54
    PH_RESERVED_OFFSET = 0x58  # 36 bytes (0x58 - 0x7B)
    PH_HEADER_CHECKSUM_OFFSET = 0x7C
    
    # Partition header size
    PARTITION_HEADER_SIZE = 128  # 0x80 bytes
    
    # Encryption key source values
    KEY_SOURCE_UNENCRYPTED = 0x00000000
    KEY_SOURCE_EFUSE_RED = 0xA5C3C5A3
    KEY_SOURCE_EFUSE_BLACK = 0xA5C3C5A5
    KEY_SOURCE_BBRAM_RED = 0x3A5C3C5A
    KEY_SOURCE_BBRAM_BLACK = 0x3A5C3C59
    KEY_SOURCE_BH_BLACK = 0xA35C7C53
    KEY_SOURCE_USER_KEY_0 = 0xC5C3A5A3
    KEY_SOURCE_USER_KEY_1 = 0xC3A5C5B3
    KEY_SOURCE_USER_KEY_2 = 0xC5C3A5C3
    KEY_SOURCE_USER_KEY_3 = 0xC3A5C5D3
    KEY_SOURCE_USER_KEY_4 = 0xC5C3A5E3
    KEY_SOURCE_USER_KEY_5 = 0xC3A5C5F3
    KEY_SOURCE_USER_KEY_6 = 0xC5C3A503
    KEY_SOURCE_USER_KEY_7 = 0xC3A5C513
    
    # Partition attribute bit shifts
    PH_TRUSTZONE_SHIFT = 0
    PH_EXCEPTION_LEVEL_SHIFT = 1
    PH_A72_EXEC_STATE_SHIFT = 3
    PH_DEST_CPU_SHIFT = 8
    PH_CHECKSUM_SHIFT = 12
    PH_PUF_HD_LOCATION_SHIFT = 14
    PH_OWNER_SHIFT = 16
    PH_ENDIANNESS_SHIFT = 18
    PH_HIVEC_SHIFT = 23
    PH_PARTITION_TYPE_SHIFT = 24
    PH_DPACM_ENABLE_SHIFT = 27
    
    # Partition attribute bit masks
    PH_TRUSTZONE_MASK = 0x1
    PH_EXCEPTION_LEVEL_MASK = 0x3
    PH_A72_EXEC_STATE_MASK = 0x1
    PH_DEST_CPU_MASK = 0xF
    PH_CHECKSUM_MASK = 0x3
    PH_PUF_HD_LOCATION_MASK = 0x3
    PH_OWNER_MASK = 0x3
    PH_ENDIANNESS_MASK = 0x1
    PH_HIVEC_MASK = 0x1
    PH_PARTITION_TYPE_MASK = 0x7
    PH_DPACM_ENABLE_MASK = 0x3

    class PartitionOwner(IntEnum):
        """Partition owner types (bits 17:16)."""
        PLM = 0
        NON_PLM = 1
        RESERVED_2 = 2
        RESERVED_3 = 3

    class ChecksumType(IntEnum):
        """Checksum types (bits 13:12)."""
        NONE = 0
        RESERVED_1 = 1
        RESERVED_2 = 2
        SHA3 = 3

    class DestinationCpu(IntEnum):
        """Destination CPU types (bits 11:8)."""
        NONE = 0
        A72_0 = 1
        A72_1 = 2
        RESERVED_3 = 3
        RESERVED_4 = 4
        R5_0 = 5
        R5_1 = 6
        R5_LOCKSTEP = 7
        PSM = 8
        AIE = 9
        RESERVED_10 = 10
        RESERVED_11 = 11
        RESERVED_12 = 12
        RESERVED_13 = 13
        RESERVED_14 = 14
        RESERVED_15 = 15

    class PartitionType(IntEnum):
        """Partition type (bits 26:24)."""
        RESERVED_0 = 0
        ELF = 1
        CDO = 2
        CFRAME = 3
        RAW = 4
        RAW_ELF = 5
        CFI_GSR_UNMASK = 6
        CFI_GSR_MASK = 7

    class ExceptionLevel(IntEnum):
        """Exception level types (bits 2:1)."""
        EL0 = 0
        EL1 = 1
        EL2 = 2
        EL3 = 3

    class A72ExecState(IntEnum):
        """A72 execution state types (bit 3)."""
        AARCH64 = 0
        AARCH32 = 1

    class VectorLocation(IntEnum):
        """Vector location types (bit 23)."""
        LOVEC = 0
        HIVEC = 1

    class Endianness(IntEnum):
        """Endianness types (bit 18)."""
        LITTLE_ENDIAN = 0
        BIG_ENDIAN = 1

    class Trustzone(IntEnum):
        """Trustzone types (bit 0)."""
        NON_SECURE = 0
        SECURE = 1

    class PufHdLocation(IntEnum):
        """PUF HD Location types (bits 15:14)."""
        EFUSE = 0
        BOOT_HEADER = 1

    def __init__(self):
        """Initialize the Versal partition header."""
        super().__init__()
        self.encrypted_partition_length: Optional[int] = None  # 0x00 (word length)
        self.unencrypted_partition_length: Optional[int] = None  # 0x04 (word length)
        self.total_partition_word_length: Optional[int] = None  # 0x08 (word length)
        self.next_partition_header_offset: Optional[int] = None  # 0x0C (word offset)
        self.destination_execution_address_lo: Optional[int] = None  # 0x10 (low 32-bits)
        self.destination_execution_address_hi: Optional[int] = None  # 0x14 (high 32-bits)
        self.destination_load_address_lo: Optional[int] = None  # 0x18 (low 32-bits)
        self.destination_load_address_hi: Optional[int] = None  # 0x1C (high 32-bits)
        self.data_word_offset: Optional[int] = None  # 0x20 (word offset)
        self.attribute_bits: Optional[int] = None  # 0x24
        self.section_count: Optional[int] = None  # 0x28
        self.checksum_word_offset: Optional[int] = None  # 0x2C (word offset)
        self.partition_id: Optional[int] = None  # 0x30
        self.authentication_certificate_word_offset: Optional[int] = None  # 0x34 (word offset)
        self.iv: Optional[bytes] = None  # 0x38-0x43 (12 bytes)
        self.encryption_key_select: Optional[int] = None  # 0x44
        self.black_iv: Optional[bytes] = None  # 0x48-0x53 (12 bytes)
        self.revoke_id: Optional[int] = None  # 0x54
        # Reserved 0x58-0x7B
        self.header_checksum: Optional[int] = None  # 0x7C

    def get_destination_execution_address(self) -> int:
        """Get the full 64-bit destination execution address.
        
        Returns:
            The 64-bit execution address.
        """
        lo = self.destination_execution_address_lo or 0
        hi = self.destination_execution_address_hi or 0
        return (hi << 32) | lo

    def get_destination_load_address(self) -> int:
        """Get the full 64-bit destination load address.
        
        Returns:
            The 64-bit load address.
        """
        lo = self.destination_load_address_lo or 0
        hi = self.destination_load_address_hi or 0
        return (hi << 32) | lo

    def get_trustzone(self) -> 'VersalPartitionHeader.Trustzone':
        """Extract trustzone from attribute bits (bit 0).
        
        Returns:
            Trustzone enum value.
        """
        if self.attribute_bits is None:
            return VersalPartitionHeader.Trustzone.NON_SECURE
        return VersalPartitionHeader.Trustzone(self.attribute_bits & 0x1)

    def get_exception_level(self) -> 'VersalPartitionHeader.ExceptionLevel':
        """Extract exception level from attribute bits (bits 2:1).
        
        Returns:
            ExceptionLevel enum value.
        """
        if self.attribute_bits is None:
            return VersalPartitionHeader.ExceptionLevel.EL3
        return VersalPartitionHeader.ExceptionLevel((self.attribute_bits >> 1) & 0x3)

    def get_a72_exec_state(self) -> 'VersalPartitionHeader.A72ExecState':
        """Extract A72 execution state from attribute bits (bit 3).
        
        Returns:
            A72ExecState enum value.
        """
        if self.attribute_bits is None:
            return VersalPartitionHeader.A72ExecState.AARCH64
        return VersalPartitionHeader.A72ExecState((self.attribute_bits >> 3) & 0x1)

    def get_destination_cpu(self) -> 'VersalPartitionHeader.DestinationCpu':
        """Extract destination CPU from attribute bits (bits 11:8).
        
        Returns:
            DestinationCpu enum value.
        """
        if self.attribute_bits is None:
            return VersalPartitionHeader.DestinationCpu.NONE
        return VersalPartitionHeader.DestinationCpu((self.attribute_bits >> 8) & 0xF)

    def get_checksum_type(self) -> 'VersalPartitionHeader.ChecksumType':
        """Extract checksum type from attribute bits (bits 13:12).
        
        Returns:
            ChecksumType enum value.
        """
        if self.attribute_bits is None:
            return VersalPartitionHeader.ChecksumType.NONE
        return VersalPartitionHeader.ChecksumType((self.attribute_bits >> 12) & 0x3)

    def get_puf_hd_location(self) -> 'VersalPartitionHeader.PufHdLocation':
        """Extract PUF HD location from attribute bits (bits 15:14).
        
        Returns:
            PufHdLocation enum value.
        """
        if self.attribute_bits is None:
            return VersalPartitionHeader.PufHdLocation.EFUSE
        return VersalPartitionHeader.PufHdLocation((self.attribute_bits >> 14) & 0x1)

    def get_partition_owner(self) -> 'VersalPartitionHeader.PartitionOwner':
        """Extract partition owner from attribute bits (bits 17:16).
        
        Returns:
            PartitionOwner enum value.
        """
        if self.attribute_bits is None:
            return VersalPartitionHeader.PartitionOwner.PLM
        return VersalPartitionHeader.PartitionOwner((self.attribute_bits >> 16) & 0x3)

    def get_endianness(self) -> 'VersalPartitionHeader.Endianness':
        """Extract endianness from attribute bits (bit 18).
        
        Returns:
            Endianness enum value.
        """
        if self.attribute_bits is None:
            return VersalPartitionHeader.Endianness.LITTLE_ENDIAN
        return VersalPartitionHeader.Endianness((self.attribute_bits >> 18) & 0x1)

    def get_vector_location(self) -> 'VersalPartitionHeader.VectorLocation':
        """Extract vector location from attribute bits (bit 23).
        
        Returns:
            VectorLocation enum value.
        """
        if self.attribute_bits is None:
            return VersalPartitionHeader.VectorLocation.LOVEC
        return VersalPartitionHeader.VectorLocation((self.attribute_bits >> 23) & 0x1)

    def get_partition_type(self) -> 'VersalPartitionHeader.PartitionType':
        """Extract partition type from attribute bits (bits 26:24).
        
        Returns:
            PartitionType enum value.
        """
        if self.attribute_bits is None:
            return VersalPartitionHeader.PartitionType.RESERVED_0
        return VersalPartitionHeader.PartitionType((self.attribute_bits >> 24) & 0x7)

    def get_dpacm_enabled(self) -> bool:
        """Check if DPA counter measure is enabled (bits 28:27).
        
        Returns:
            True if DPACM is enabled (value is non-zero), False otherwise.
        """
        if self.attribute_bits is None:
            return False
        return bool((self.attribute_bits >> 27) & 0x3)

    def get_encryption_present(self) -> bool:
        """Check if partition is encrypted.
        
        Returns:
            True if partition is encrypted, False otherwise.
        """
        if self.encryption_key_select is None:
            return False
        return self.encryption_key_select != self.KEY_SOURCE_UNENCRYPTED

    def get_attribute_summary(self) -> str:
        """Get a human-readable summary of the attribute bits.
        
        Returns:
            String describing the partition attributes.
        """
        return (f"Type={self.get_partition_type().name}, "
                f"Owner={self.get_partition_owner().name}, "
                f"CPU={self.get_destination_cpu().name}, "
                f"TZ={self.get_trustzone().name}, "
                f"EL={self.get_exception_level().name}, "
                f"ExecState={self.get_a72_exec_state().name}")

    def get_encrypted_partition_length_bytes(self) -> int:
        """Get the encrypted partition length in bytes.
        
        The length is stored in words (4 bytes each) in the header.
        
        Returns:
            The encrypted partition length in bytes.
        """
        if self.encrypted_partition_length is None:
            return 0
        return self.encrypted_partition_length * 4

    def get_unencrypted_partition_length_bytes(self) -> int:
        """Get the unencrypted partition length in bytes.
        
        The length is stored in words (4 bytes each) in the header.
        
        Returns:
            The unencrypted partition length in bytes.
        """
        if self.unencrypted_partition_length is None:
            return 0
        return self.unencrypted_partition_length * 4

    def get_total_partition_length_bytes(self) -> int:
        """Get the total partition length in bytes.
        
        The length is stored in words (4 bytes each) in the header.
        
        Returns:
            The total partition length in bytes.
        """
        if self.total_partition_word_length is None:
            return 0
        return self.total_partition_word_length * 4

    def get_data_offset_bytes(self) -> int:
        """Get the data offset in bytes.
        
        The offset is stored in words (4 bytes each) in the header.
        
        Returns:
            The data offset in bytes.
        """
        if self.data_word_offset is None:
            return 0
        return self.data_word_offset * 4

    def compute_checksum(self) -> int:
        """Compute the checksum over the partition header fields.
        
        The checksum is the sum of all 32-bit words in the header,
        excluding the checksum field itself (last 4 bytes).
        
        Returns:
            The computed checksum as a 32-bit unsigned integer.
        """
        # Pack the header fields into words (excluding checksum)
        words = [
            self.encrypted_partition_length or 0,          # 0x00
            self.unencrypted_partition_length or 0,        # 0x04
            self.total_partition_word_length or 0,         # 0x08
            self.next_partition_header_offset or 0,        # 0x0C
            self.destination_execution_address_lo or 0,    # 0x10
            self.destination_execution_address_hi or 0,    # 0x14
            self.destination_load_address_lo or 0,         # 0x18
            self.destination_load_address_hi or 0,         # 0x1C
            self.data_word_offset or 0,                    # 0x20
            self.attribute_bits or 0,                      # 0x24
            self.section_count or 0,                       # 0x28
            self.checksum_word_offset or 0,                # 0x2C
            self.partition_id or 0,                        # 0x30
            self.authentication_certificate_word_offset or 0,  # 0x34
        ]
        
        # Add IV (12 bytes = 3 words)
        if self.iv:
            for i in range(0, 12, 4):
                words.append(int.from_bytes(self.iv[i:i+4], 'little'))
        else:
            words.extend([0] * 3)
        
        # Add encryption key select
        words.append(self.encryption_key_select or 0)  # 0x44
        
        # Add black IV (12 bytes = 3 words)
        if self.black_iv:
            for i in range(0, 12, 4):
                words.append(int.from_bytes(self.black_iv[i:i+4], 'little'))
        else:
            words.extend([0] * 3)
        
        # Add revoke ID
        words.append(self.revoke_id or 0)  # 0x54
        
        # Add reserved area (0x58-0x7B = 36 bytes = 9 words)
        words.extend([0] * 9)
        
        # Sum all words and mask to 32 bits
        checksum = sum(words) & 0xFFFFFFFF
        
        # Invert (one's complement)
        checksum = (~checksum) & 0xFFFFFFFF
        
        return checksum

    def verify_checksum(self) -> bool:
        """Verify the partition header checksum.
        
        Returns:
            True if the checksum is valid, False otherwise.
        """
        if self.header_checksum is None:
            return False
        return self.compute_checksum() == self.header_checksum


class VersalBootImageLoader(BootImageLoader):
    """Versal-specific boot image loader implementation."""

    def __init__(self, image_path=None, image_path_offset=0):
        """Initialize the Versal boot image loader.
        
        Args:
            image_path: Path to the boot image file (PDI).
            image_path_offset: Offset within the image file to start reading from.
        """
        super().__init__(image_path, image_path_offset)
        self.boot_header = VersalBootHeader()
        self.image_header_table = VersalImageHeaderTable()
        self.image = []  # List of tuples (image_header, [partition_headers])
        self.optional_data: List[VersalOptionalData] = []  # List of optional data entries

    @property
    def image_headers(self):
        """Property that provides image_headers with partition_headers attribute.
        
        This provides compatibility with code that expects image_headers to be a list
        where each item has a partition_headers attribute.
        
        Returns:
            List of image header wrappers with partition_headers attribute.
        """
        return [ImageHeaderWrapper(img_hdr, part_hdrs) for img_hdr, part_hdrs in self.image]

    def verify_magic(self) -> bool:
        """Verify the magic bytes at offset 0x14.
        
        Checks for "XLNX" (0x584c4e58 in little endian) at offset 0x14.
        
        Returns:
            True if the magic bytes are correct, False otherwise.
        
        Raises:
            RuntimeError: If no image data is loaded.
            ValueError: If image data is too small.
        """
        return self._verify_magic_at_offset(0x14)

    def detect_width(self) -> bool:
        """Detect the bus width at offset 0x10.
        
        Detects QSPI bus width at offset 0x10.
        
        Returns:
            True if the width detection is successful, False otherwise.
        
        Raises:
            RuntimeError: If no image data is loaded.
            ValueError: If image data is too small.
        """
        return self._detect_width_at_offset(0x10)

    def parse(self):
        """Parse the Versal boot image (PDI).
        
        Raises:
            RuntimeError: If no image data is loaded.
        """
        if not self.image_fd:
            raise RuntimeError("No image data loaded. Call open() first.")
        
        # Validate the boot header before reading too much

        # Image Identification at 0x14 (already read by verify_magic)
        if not self.verify_magic():
            raise ValueError(f"Invalid boot image: magic bytes do not match {self.boot_header.header_signature.hex()}")
        
        if not self.detect_width():
            raise ValueError(f"Width detection indicates an unsupported configuration: {self.boot_header.width_detection_word.hex()}") 
        
        # Read SelectMAP bus width at 0x00-0x0F (16 bytes)
        self._safe_seek(self.image_path_offset + 0x00, 16)
        self.boot_header.smap_bus_width = self.image_fd.read(16)
        
        # QSPI bus width at 0x10 (4 bytes) - already read by detect_width
        self._safe_seek(self.image_path_offset + 0x10, 4)
        self.boot_header.qspi_bus_width = struct.unpack('<I', self.image_fd.read(4))[0]
        
        # Image Identification at 0x14 (4 bytes) - already read by verify_magic
        self._safe_seek(self.image_path_offset + 0x14, 4)
        self.boot_header.image_identification = struct.unpack('<I', self.image_fd.read(4))[0]
        
        # Encryption Key Source at 0x18 (4 bytes)
        self._safe_seek(self.image_path_offset + 0x18, 4)
        self.boot_header.encryption_key_source = struct.unpack('<I', self.image_fd.read(4))[0]
        if self.boot_header.encryption_key_source != VersalBootHeader.KEY_SOURCE_UNENCRYPTED:
            raise ValueError("Encryption is not supported")
        
        # PLM Source Offset at 0x1C (4 bytes, little-endian)
        self._safe_seek(self.image_path_offset + 0x1C, 4)
        self.boot_header.plm_source_offset = struct.unpack('<I', self.image_fd.read(4))[0]
        
        # PMC Data Load Address at 0x20 (4 bytes, little-endian)
        self._safe_seek(self.image_path_offset + 0x20, 4)
        self.boot_header.pmc_data_load_address = struct.unpack('<I', self.image_fd.read(4))[0]
        
        # PMC Data Length at 0x24 (4 bytes, little-endian)
        self._safe_seek(self.image_path_offset + 0x24, 4)
        self.boot_header.pmc_data_length = struct.unpack('<I', self.image_fd.read(4))[0]
        
        # Total PMC Data Length at 0x28 (4 bytes, little-endian)
        self._safe_seek(self.image_path_offset + 0x28, 4)
        self.boot_header.total_pmc_data_length = struct.unpack('<I', self.image_fd.read(4))[0]
        
        # PLM Length at 0x2C (4 bytes, little-endian)
        self._safe_seek(self.image_path_offset + 0x2C, 4)
        self.boot_header.plm_length = struct.unpack('<I', self.image_fd.read(4))[0]
        
        # Total PLM Length at 0x30 (4 bytes, little-endian)
        self._safe_seek(self.image_path_offset + 0x30, 4)
        self.boot_header.total_plm_length = struct.unpack('<I', self.image_fd.read(4))[0]
        
        # Boot Header Attributes at 0x34 (4 bytes, little-endian)
        self._safe_seek(self.image_path_offset + 0x34, 4)
        self.boot_header.boot_header_attributes = struct.unpack('<I', self.image_fd.read(4))[0]
        
        # Black Key at 0x38-0x57 (32 bytes)
        self._safe_seek(self.image_path_offset + 0x38, 32)
        self.boot_header.black_key = self.image_fd.read(32)
        
        # Black IV at 0x58-0x63 (12 bytes)
        self._safe_seek(self.image_path_offset + 0x58, 12)
        self.boot_header.black_iv = self.image_fd.read(12)
        
        # Secure Header IV at 0x64-0x6F (12 bytes)
        self._safe_seek(self.image_path_offset + 0x64, 12)
        self.boot_header.secure_header_iv = self.image_fd.read(12)
        
        # PUF Shutter Value at 0x70 (4 bytes, little-endian)
        self._safe_seek(self.image_path_offset + 0x70, 4)
        self.boot_header.puf_shutter_value = struct.unpack('<I', self.image_fd.read(4))[0]
        
        # Secure Header IV for PMC Data at 0x74-0x7F (12 bytes)
        self._safe_seek(self.image_path_offset + 0x74, 12)
        self.boot_header.secure_header_iv_pmc_data = self.image_fd.read(12)
        
        # Reserved at 0x80-0xC3 (68 bytes) - skip
        
        # Meta Header Offset at 0xC4 (4 bytes, little-endian)
        self._safe_seek(self.image_path_offset + 0xC4, 4)
        self.boot_header.meta_header_offset = struct.unpack('<I', self.image_fd.read(4))[0]
        
        # Reserved at 0xC8-0x127 (96 bytes) - skip
        
        # Register Init at 0x128-0x927 (2048 bytes)
        self._safe_seek(self.image_path_offset + 0x128, 2048)
        self.boot_header.register_init = self.image_fd.read(2048)
        
        # PUF Helper Data at 0x928-0xF2F (1544 bytes)
        self._safe_seek(self.image_path_offset + 0x928, 1544)
        self.boot_header.puf_helper_data = self.image_fd.read(1544)
        
        # Boot Header Checksum at 0xF30 (4 bytes, little-endian)
        self._safe_seek(self.image_path_offset + 0xF30, 4)
        self.boot_header.boot_header_checksum = struct.unpack('<I', self.image_fd.read(4))[0]
        
        # SHA3 Padding at 0xF34-0xF7F (76 bytes)
        self._safe_seek(self.image_path_offset + 0xF34, 76)
        self.boot_header.sha3_padding = self.image_fd.read(76)
        
        # Verify boot header checksum
        if not self.boot_header.verify_checksum():
            computed = self.boot_header.compute_checksum()
            expected = self.boot_header.boot_header_checksum
            raise ValueError(
                f"Boot header checksum verification failed: computed=0x{computed:08X}, expected=0x{expected:08X}"
            )

        # Load Image Header Table from meta_header_offset
        if self.boot_header.meta_header_offset is not None and self.boot_header.meta_header_offset != 0:
            iht_offset = self.boot_header.meta_header_offset
            
            # Seek to Image Header Table offset
            self._safe_seek(self.image_path_offset + iht_offset, VersalImageHeaderTable.IMAGE_HEADER_TABLE_SIZE)
            
            # Version at 0x00 (4 bytes)
            self.image_header_table.version = struct.unpack('<I', self.image_fd.read(4))[0]
            
            # Validate version
            if not self.image_header_table.is_version_valid():
                raise ValueError(f"Invalid Image Header Table version: 0x{self.image_header_table.version:08X}")
            
            # Total Image Count at 0x04 (4 bytes, little-endian)
            self.image_header_table.total_image_count = struct.unpack('<I', self.image_fd.read(4))[0]
            
            # Image Header Offset at 0x08 (4 bytes, little-endian, word offset)
            self.image_header_table.image_header_offset = struct.unpack('<I', self.image_fd.read(4))[0]
            
            # Total Partition Count at 0x0C (4 bytes, little-endian)
            self.image_header_table.total_partition_count = struct.unpack('<I', self.image_fd.read(4))[0]
            
            # Partition Header Offset at 0x10 (4 bytes, little-endian, word offset)
            self.image_header_table.partition_header_offset = struct.unpack('<I', self.image_fd.read(4))[0]
            
            # Secondary Boot Device Address at 0x14 (4 bytes, little-endian)
            self.image_header_table.secondary_boot_device_address = struct.unpack('<I', self.image_fd.read(4))[0]
            
            # Reserved at 0x18 (4 bytes) - skip
            _ = self.image_fd.read(4)
            
            # Attributes at 0x1C (4 bytes, little-endian)
            self.image_header_table.attributes = struct.unpack('<I', self.image_fd.read(4))[0]
            
            # PDI ID at 0x20 (4 bytes, little-endian)
            self.image_header_table.pdi_id = struct.unpack('<I', self.image_fd.read(4))[0]
            
            # Parent ID at 0x24 (4 bytes, little-endian)
            self.image_header_table.parent_id = struct.unpack('<I', self.image_fd.read(4))[0]
            
            # Identification String at 0x28 (4 bytes, little-endian)
            self.image_header_table.identification_string = struct.unpack('<I', self.image_fd.read(4))[0]
            
            # Headers Size at 0x2C (4 bytes, little-endian)
            self.image_header_table.headers_size = struct.unpack('<I', self.image_fd.read(4))[0]
            
            # Total Meta Header Length at 0x30 (4 bytes, little-endian)
            self.image_header_table.total_meta_header_length = struct.unpack('<I', self.image_fd.read(4))[0]
            
            # Meta Header IV at 0x34-0x3F (12 bytes)
            self.image_header_table.meta_header_iv = self.image_fd.read(12)
            
            # Encryption Status at 0x40 (4 bytes, little-endian)
            self.image_header_table.encryption_status = struct.unpack('<I', self.image_fd.read(4))[0]
            
            # Reserved at 0x44 (4 bytes) - skip
            _ = self.image_fd.read(4)
            
            # Meta Header AC Offset at 0x48 (4 bytes, little-endian, word offset)
            self.image_header_table.meta_header_ac_offset = struct.unpack('<I', self.image_fd.read(4))[0]
            
            # Meta Header Black IV at 0x4C-0x57 (12 bytes)
            self.image_header_table.meta_header_black_iv = self.image_fd.read(12)
            
            # Optional Data Length at 0x58 (4 bytes, little-endian, words)
            self.image_header_table.optional_data_length = struct.unpack('<I', self.image_fd.read(4))[0]
            
            # Reserved at 0x5C-0x7B (32 bytes) - skip
            _ = self.image_fd.read(32)
            
            # Checksum at 0x7C (4 bytes, little-endian)
            self.image_header_table.checksum = struct.unpack('<I', self.image_fd.read(4))[0]
            
            # Parse optional data if present
            if self.image_header_table.optional_data_length and self.image_header_table.optional_data_length > 0:
                self._parse_optional_data()

        # Load Image Headers and corresponding Partition Headers
        if self.image_header_table.image_header_offset is not None:
            current_offset = self.image_header_table.image_header_offset * 4  # Word to byte offset
            
            # Get the actual image header size from headers_size field
            ih_size_words = self.image_header_table.get_ih_size()
            ih_size_bytes = ih_size_words * 4 if ih_size_words > 0 else VersalImageHeader.IMAGE_HEADER_SIZE
            
            for _ in range(self.image_header_table.total_image_count or 0):
                if current_offset == 0:
                    break
                
                # Create a new image header instance
                image_header = VersalImageHeader()
                
                # Seek to current image header offset
                self._safe_seek(self.image_path_offset + current_offset, ih_size_bytes)
                
                # First Partition Header Offset at 0x00 (4 bytes, little-endian, word offset)
                image_header.first_partition_header_offset = struct.unpack('<I', self.image_fd.read(4))[0]
                
                # Partition Count at 0x04 (4 bytes, little-endian)
                image_header.partition_count = struct.unpack('<I', self.image_fd.read(4))[0]
                
                # Revoke ID at 0x08 (4 bytes, little-endian)
                image_header.revoke_id = struct.unpack('<I', self.image_fd.read(4))[0]
                
                # Attributes at 0x0C (4 bytes, little-endian)
                image_header.attributes = struct.unpack('<I', self.image_fd.read(4))[0]
                
                # Image Name at 0x10-0x1F (16 bytes, null-terminated)
                image_name_bytes = self.image_fd.read(16)
                # Find null terminator
                null_idx = image_name_bytes.find(b'\x00')
                if null_idx >= 0:
                    image_header.image_name = image_name_bytes[:null_idx].decode('ascii', errors='ignore')
                else:
                    image_header.image_name = image_name_bytes.decode('ascii', errors='ignore')
                
                # Image/Node ID at 0x20 (4 bytes, little-endian)
                image_header.image_node_id = struct.unpack('<I', self.image_fd.read(4))[0]
                
                # Unique ID at 0x24 (4 bytes, little-endian)
                image_header.unique_id = struct.unpack('<I', self.image_fd.read(4))[0]
                
                # Parent Unique ID at 0x28 (4 bytes, little-endian)
                image_header.parent_unique_id = struct.unpack('<I', self.image_fd.read(4))[0]
                
                # Function ID at 0x2C (4 bytes, little-endian)
                image_header.function_id = struct.unpack('<I', self.image_fd.read(4))[0]
                
                # DDR Low Address at 0x30 (4 bytes, little-endian)
                image_header.ddr_low_address = struct.unpack('<I', self.image_fd.read(4))[0]
                
                # DDR High Address at 0x34 (4 bytes, little-endian)
                image_header.ddr_high_address = struct.unpack('<I', self.image_fd.read(4))[0]
                
                # Reserved at 0x38 (4 bytes) - skip
                _ = self.image_fd.read(4)
                
                # Checksum at 0x3C (4 bytes, little-endian)
                image_header.checksum = struct.unpack('<I', self.image_fd.read(4))[0]
                
                # Load all corresponding partition headers for this image
                partition_headers = []
                partition_header_offset = image_header.first_partition_header_offset * 4  # Convert word to byte offset
                partitions_to_read = image_header.partition_count or 1
                
                # Get the actual partition header size from headers_size field
                ph_size_words = self.image_header_table.get_ph_size()
                ph_size_bytes = ph_size_words * 4 if ph_size_words > 0 else VersalPartitionHeader.PARTITION_HEADER_SIZE
                
                for _ in range(partitions_to_read):
                    if partition_header_offset == 0:
                        break
                    
                    partition_header = VersalPartitionHeader()
                    
                    # Seek to partition header location
                    self._safe_seek(self.image_path_offset + partition_header_offset, ph_size_bytes)
                    
                    # Encrypted Partition Length at 0x00 (4 bytes, little-endian)
                    partition_header.encrypted_partition_length = struct.unpack('<I', self.image_fd.read(4))[0]
                    
                    # Unencrypted Partition Length at 0x04 (4 bytes, little-endian)
                    partition_header.unencrypted_partition_length = struct.unpack('<I', self.image_fd.read(4))[0]
                    
                    # Total Partition Word Length at 0x08 (4 bytes, little-endian)
                    partition_header.total_partition_word_length = struct.unpack('<I', self.image_fd.read(4))[0]
                    
                    # Next Partition Header Offset at 0x0C (4 bytes, little-endian, word offset)
                    partition_header.next_partition_header_offset = struct.unpack('<I', self.image_fd.read(4))[0]
                    
                    # Destination Execution Address LO at 0x10 (4 bytes, little-endian)
                    partition_header.destination_execution_address_lo = struct.unpack('<I', self.image_fd.read(4))[0]
                    
                    # Destination Execution Address HI at 0x14 (4 bytes, little-endian)
                    partition_header.destination_execution_address_hi = struct.unpack('<I', self.image_fd.read(4))[0]
                    
                    # Destination Load Address LO at 0x18 (4 bytes, little-endian)
                    partition_header.destination_load_address_lo = struct.unpack('<I', self.image_fd.read(4))[0]
                    
                    # Destination Load Address HI at 0x1C (4 bytes, little-endian)
                    partition_header.destination_load_address_hi = struct.unpack('<I', self.image_fd.read(4))[0]
                    
                    # Data Word Offset at 0x20 (4 bytes, little-endian)
                    partition_header.data_word_offset = struct.unpack('<I', self.image_fd.read(4))[0]
                    
                    # Attribute Bits at 0x24 (4 bytes, little-endian)
                    partition_header.attribute_bits = struct.unpack('<I', self.image_fd.read(4))[0]
                    
                    # Section Count at 0x28 (4 bytes, little-endian)
                    partition_header.section_count = struct.unpack('<I', self.image_fd.read(4))[0]
                    
                    # Checksum Word Offset at 0x2C (4 bytes, little-endian)
                    partition_header.checksum_word_offset = struct.unpack('<I', self.image_fd.read(4))[0]
                    
                    # Partition ID at 0x30 (4 bytes, little-endian)
                    partition_header.partition_id = struct.unpack('<I', self.image_fd.read(4))[0]
                    
                    # Authentication Certificate Word Offset at 0x34 (4 bytes, little-endian)
                    partition_header.authentication_certificate_word_offset = struct.unpack('<I', self.image_fd.read(4))[0]
                    
                    # IV at 0x38-0x43 (12 bytes)
                    partition_header.iv = self.image_fd.read(12)
                    
                    # Encryption Key Select at 0x44 (4 bytes, little-endian)
                    partition_header.encryption_key_select = struct.unpack('<I', self.image_fd.read(4))[0]
                    
                    # Black IV at 0x48-0x53 (12 bytes)
                    partition_header.black_iv = self.image_fd.read(12)
                    
                    # Revoke ID at 0x54 (4 bytes, little-endian)
                    partition_header.revoke_id = struct.unpack('<I', self.image_fd.read(4))[0]
                    
                    # Reserved at 0x58-0x7B (36 bytes) - skip
                    _ = self.image_fd.read(36)
                    
                    # Header Checksum at 0x7C (4 bytes, little-endian)
                    partition_header.header_checksum = struct.unpack('<I', self.image_fd.read(4))[0]
                    
                    # Verify partition header checksum
                    if not partition_header.verify_checksum():
                        raise ValueError(f"Partition header checksum verification failed for '{image_header.image_name}'")
                    
                    # Check for encrypted partition
                    if partition_header.get_encryption_present():
                        raise ValueError(f"Partition '{image_header.image_name}' is encrypted. Encrypted partitions are not supported.")
                    
                    partition_headers.append(partition_header)
                    
                    # Move to next partition header for this image
                    partition_header_offset = partition_header.next_partition_header_offset * 4  # Word to byte offset
                
                # Add tuple to list (image_header, list of partition_headers)
                self.image.append((image_header, partition_headers))
                
                # Move to next image header
                current_offset += ih_size_bytes

        return

    def debug(self):
        """Output debug information about the loaded Versal boot image (PDI)."""
        if not self.image_fd:
            raise RuntimeError("No image data loaded. Call open() first.")
        
        print("Versal Boot Image (PDI) Header:")
        print(f"  SelectMAP Bus Width: {self.boot_header.smap_bus_width.hex() if self.boot_header.smap_bus_width else 'Not read'}")
        print(f"  QSPI Bus Width: {hex(self.boot_header.qspi_bus_width) if self.boot_header.qspi_bus_width is not None else 'Not read'}")
        print(f"  Image Identification: {hex(self.boot_header.image_identification) if self.boot_header.image_identification is not None else 'Not read'}")
        print(f"  Encryption Key Source: {hex(self.boot_header.encryption_key_source) if self.boot_header.encryption_key_source is not None else 'Not read'}")
        print(f"  PLM Source Offset: {hex(self.boot_header.plm_source_offset) if self.boot_header.plm_source_offset is not None else 'Not read'}")
        print(f"  PMC Data Load Address: {hex(self.boot_header.pmc_data_load_address) if self.boot_header.pmc_data_load_address is not None else 'Not read'}")
        print(f"  PMC Data Length: {self.boot_header.pmc_data_length if self.boot_header.pmc_data_length is not None else 'Not read'}")
        print(f"  Total PMC Data Length: {self.boot_header.total_pmc_data_length if self.boot_header.total_pmc_data_length is not None else 'Not read'}")
        print(f"  PLM Length: {self.boot_header.plm_length if self.boot_header.plm_length is not None else 'Not read'}")
        print(f"  Total PLM Length: {self.boot_header.total_plm_length if self.boot_header.total_plm_length is not None else 'Not read'}")
        print(f"  Boot Header Attributes: {hex(self.boot_header.boot_header_attributes) if self.boot_header.boot_header_attributes is not None else 'Not read'}")
        if self.boot_header.boot_header_attributes is not None:
            print(f"    PUF Mode: {self.boot_header.get_puf_mode().name}")
            print(f"    BH Authentication: {self.boot_header.get_bh_authentication().name}")
            print(f"    DPA Counter Measure: {self.boot_header.get_dpa_cm().name}")
            print(f"    Checksum Type: {self.boot_header.get_checksum_type().name}")
            print(f"    PUF HD Location: {self.boot_header.get_puf_hd_location().name}")
        print(f"  Black Key: {self.boot_header.black_key.hex() if self.boot_header.black_key else 'Not read'}")
        print(f"  Black IV: {self.boot_header.black_iv.hex() if self.boot_header.black_iv else 'Not read'}")
        print(f"  Secure Header IV: {self.boot_header.secure_header_iv.hex() if self.boot_header.secure_header_iv else 'Not read'}")
        print(f"  PUF Shutter Value: {hex(self.boot_header.puf_shutter_value) if self.boot_header.puf_shutter_value is not None else 'Not read'}")
        print(f"  Secure Header IV (PMC Data): {self.boot_header.secure_header_iv_pmc_data.hex() if self.boot_header.secure_header_iv_pmc_data else 'Not read'}")
        print(f"  Meta Header Offset: {hex(self.boot_header.meta_header_offset) if self.boot_header.meta_header_offset is not None else 'Not read'}")
        print(f"  Boot Header Checksum: {hex(self.boot_header.boot_header_checksum) if self.boot_header.boot_header_checksum is not None else 'Not read'}")
        
        if self.boot_header.meta_header_offset:
            print("\nVersal Image Header Table:")
            print(f"  Version: {hex(self.image_header_table.version) if self.image_header_table.version else 'Not read'}")
            print(f"  Total Image Count: {self.image_header_table.total_image_count if self.image_header_table.total_image_count is not None else 'Not read'}")
            print(f"  Image Header Offset: {hex(self.image_header_table.image_header_offset) if self.image_header_table.image_header_offset is not None else 'Not read'} (words)")
            print(f"  Total Partition Count: {self.image_header_table.total_partition_count if self.image_header_table.total_partition_count is not None else 'Not read'}")
            print(f"  Partition Header Offset: {hex(self.image_header_table.partition_header_offset) if self.image_header_table.partition_header_offset is not None else 'Not read'} (words)")
            print(f"  Secondary Boot Device Address: {hex(self.image_header_table.secondary_boot_device_address) if self.image_header_table.secondary_boot_device_address is not None else 'Not read'}")
            print(f"  Attributes: {hex(self.image_header_table.attributes) if self.image_header_table.attributes is not None else 'Not read'}")
            if self.image_header_table.attributes is not None:
                print(f"    Secondary Boot Device: {self.image_header_table.get_secondary_boot_device().name}")
                print(f"    DPACM Enabled: {self.image_header_table.get_dpacm_enabled()}")
                print(f"    PUF HD in Boot Header: {self.image_header_table.get_puf_hd_in_bh()}")
            print(f"  PDI ID: {hex(self.image_header_table.pdi_id) if self.image_header_table.pdi_id is not None else 'Not read'}")
            print(f"  Parent ID: {hex(self.image_header_table.parent_id) if self.image_header_table.parent_id is not None else 'Not read'}")
            if self.image_header_table.identification_string is not None:
                id_bytes = self.image_header_table.identification_string.to_bytes(4, 'little')
                id_str = id_bytes.decode('ascii', errors='ignore')
                print(f"  Identification String: {id_str} ({hex(self.image_header_table.identification_string)})")
            print(f"  Headers Size: {hex(self.image_header_table.headers_size) if self.image_header_table.headers_size is not None else 'Not read'}")
            if self.image_header_table.headers_size is not None:
                print(f"    IHT Size: {self.image_header_table.get_iht_size()} words")
                print(f"    IH Size: {self.image_header_table.get_ih_size()} words")
                print(f"    PH Size: {self.image_header_table.get_ph_size()} words")
            print(f"  Total Meta Header Length: {self.image_header_table.total_meta_header_length if self.image_header_table.total_meta_header_length is not None else 'Not read'} (words)")
            print(f"  Meta Header IV: {self.image_header_table.meta_header_iv.hex() if self.image_header_table.meta_header_iv else 'Not read'}")
            print(f"  Encryption Status: {hex(self.image_header_table.encryption_status) if self.image_header_table.encryption_status is not None else 'Not read'}")
            print(f"  Meta Header AC Offset: {hex(self.image_header_table.meta_header_ac_offset) if self.image_header_table.meta_header_ac_offset is not None else 'Not read'} (words)")
            print(f"  Meta Header Black IV: {self.image_header_table.meta_header_black_iv.hex() if self.image_header_table.meta_header_black_iv else 'Not read'}")
            print(f"  Optional Data Length: {self.image_header_table.optional_data_length if self.image_header_table.optional_data_length is not None else 'Not read'} (words)")
            print(f"  Checksum: {hex(self.image_header_table.checksum) if self.image_header_table.checksum is not None else 'Not read'}")
        
        # Display Optional Data Entries
        if self.optional_data:
            print(f"\nVersal Optional Data Entries (Total: {len(self.optional_data)}):")
            for idx, opt_data in enumerate(self.optional_data):
                print(f"\n  Optional Data Entry #{idx}:")
                print(f"    Data ID: {hex(opt_data.data_id)} ({opt_data.get_data_id_name()})")
                print(f"    Size: {opt_data.size_words} words ({opt_data.size_words * 4} bytes including header and checksum)")
                data_size = len(opt_data.data) if opt_data.data else 0
                print(f"    Data Size: {data_size} bytes (excluding header and checksum)")
                if opt_data.data:
                    # Display data as hex with spaces, limit to first 64 bytes for readability
                    data_to_show = opt_data.data[:64]
                    hex_str = ' '.join(f'{b:02x}' for b in data_to_show)
                    if len(opt_data.data) <= 64:
                        print(f"    Data: {hex_str}")
                    else:
                        print(f"    Data: {hex_str} ... ({len(opt_data.data)} bytes total)")
                else:
                    print(f"    Data: (empty)")
                if opt_data.checksum is not None:
                    checksum_valid = opt_data.verify_checksum()
                    validity_str = "VALID" if checksum_valid else "INVALID"
                    print(f"    Checksum: {hex(opt_data.checksum)} ({validity_str})")
                else:
                    print(f"    Checksum: Not present")
        
        # Display Image Headers
        if self.image:
            print(f"\nVersal Image Headers (Total: {len(self.image)}):")
            for idx, (image_header, partition_headers) in enumerate(self.image):
                if idx == 0:
                    print(f"\n  Bootloader Image Header:")
                else:
                    print(f"\n  Image Header #{idx}:")
                print(f"    First Partition Header: {hex(image_header.first_partition_header_offset) if image_header.first_partition_header_offset is not None else 'Not read'} (words)")
                print(f"    Partition Count: {image_header.partition_count if image_header.partition_count is not None else 'Not read'}")
                print(f"    Revoke ID: {image_header.revoke_id if image_header.revoke_id is not None else 'Not read'}")
                print(f"    Attributes: {hex(image_header.attributes) if image_header.attributes is not None else 'Not read'}")
                if image_header.attributes is not None:
                    print(f"      Image Owner: {image_header.get_image_owner().name}")
                    print(f"      Copy to Memory: {image_header.get_copy_to_memory()}")
                    print(f"      Delay Load: {image_header.get_delay_load()}")
                    print(f"      Delay Handoff: {image_header.get_delay_handoff()}")
                print(f"    Image Name: '{image_header.image_name if image_header.image_name else 'Not read'}'")
                print(f"    Image/Node ID: {hex(image_header.image_node_id) if image_header.image_node_id is not None else 'Not read'}")
                print(f"    Unique ID: {hex(image_header.unique_id) if image_header.unique_id is not None else 'Not read'}")
                print(f"    Parent Unique ID: {hex(image_header.parent_unique_id) if image_header.parent_unique_id is not None else 'Not read'}")
                print(f"    Function ID: {hex(image_header.function_id) if image_header.function_id is not None else 'Not read'}")
                if image_header.ddr_low_address is not None and image_header.ddr_high_address is not None:
                    ddr_addr = image_header.get_ddr_address()
                    print(f"    DDR Address: {hex(ddr_addr)}")
                print(f"    Checksum: {hex(image_header.checksum) if image_header.checksum is not None else 'Not read'}")
                
                # Display all Partition Headers for this image
                for part_idx, partition_header in enumerate(partition_headers):
                    if idx == 0:
                        print(f"\n  Bootloader Partition Header:")
                    else:
                        print(f"\n  Partition Header #{idx}.{part_idx}:")
                    print(f"    Encrypted Partition Length: {partition_header.encrypted_partition_length if partition_header.encrypted_partition_length is not None else 'Not read'}")
                    print(f"    Unencrypted Partition Length: {partition_header.unencrypted_partition_length if partition_header.unencrypted_partition_length is not None else 'Not read'}")
                    print(f"    Total Partition Word Length: {partition_header.total_partition_word_length if partition_header.total_partition_word_length is not None else 'Not read'}")
                    print(f"    Next Partition Header Offset: {hex(partition_header.next_partition_header_offset) if partition_header.next_partition_header_offset is not None else 'Not read'} (words)")
                    # Display 64-bit addresses combined
                    if partition_header.destination_execution_address_hi is not None and partition_header.destination_execution_address_lo is not None:
                        exec_addr = partition_header.get_destination_execution_address()
                        print(f"    Destination Execution Address: {hex(exec_addr)}")
                    else:
                        print(f"    Destination Execution Address: Not read")
                    if partition_header.destination_load_address_hi is not None and partition_header.destination_load_address_lo is not None:
                        load_addr = partition_header.get_destination_load_address()
                        print(f"    Destination Load Address: {hex(load_addr)}")
                    else:
                        print(f"    Destination Load Address: Not read")
                    print(f"    Data Word Offset: {hex(partition_header.data_word_offset) if partition_header.data_word_offset is not None else 'Not read'}")
                    print(f"    Attribute Bits: {hex(partition_header.attribute_bits) if partition_header.attribute_bits is not None else 'Not read'}")
                    print(f"      Partition Type: {partition_header.get_partition_type().name}")
                    print(f"      Trustzone: {partition_header.get_trustzone().name}")
                    print(f"      Exception Level: {partition_header.get_exception_level().name}")
                    print(f"      A72 Exec State: {partition_header.get_a72_exec_state().name}")
                    print(f"      Destination CPU: {partition_header.get_destination_cpu().name}")
                    print(f"      Checksum Type: {partition_header.get_checksum_type().name}")
                    print(f"      PUF HD Location: {partition_header.get_puf_hd_location().name}")
                    print(f"      Owner: {partition_header.get_partition_owner().name}")
                    print(f"      Endianness: {partition_header.get_endianness().name}")
                    print(f"      Vector Location: {partition_header.get_vector_location().name}")
                    print(f"      DPACM Enabled: {partition_header.get_dpacm_enabled()}")
                    print(f"    Section Count: {partition_header.section_count if partition_header.section_count is not None else 'Not read'}")
                    print(f"    Checksum Word Offset: {hex(partition_header.checksum_word_offset) if partition_header.checksum_word_offset is not None else 'Not read'}")
                    print(f"    Partition ID: {partition_header.partition_id if partition_header.partition_id is not None else 'Not read'}")
                    print(f"    Authentication Certificate Word Offset: {hex(partition_header.authentication_certificate_word_offset) if partition_header.authentication_certificate_word_offset is not None else 'Not read'}")
                    print(f"    IV: {partition_header.iv.hex() if partition_header.iv else 'Not read'}")
                    print(f"    Encryption Key Select: {hex(partition_header.encryption_key_select) if partition_header.encryption_key_select is not None else 'Not read'}")
                    print(f"    Black IV: {partition_header.black_iv.hex() if partition_header.black_iv else 'Not read'}")
                    print(f"    Revoke ID: {partition_header.revoke_id if partition_header.revoke_id is not None else 'Not read'}")
                    print(f"    Header Checksum: {hex(partition_header.header_checksum) if partition_header.header_checksum is not None else 'Not read'}")

    def _get_first_stage_loader_length(self, data_byte_offset: int, default_length: int) -> int:
        """Return the length for PLM partition, or default_length otherwise.
        
        For Versal, the PLM (Platform Loader and Manager) partition length is stored
        in the boot header's total_plm_length field.
        
        Args:
            data_byte_offset: The data offset of the current partition.
            default_length: The default length from the partition header.
        
        Returns:
            The PLM length if this is the PLM partition, otherwise default_length.
        """
        if (self.boot_header.plm_source_offset is not None and 
            data_byte_offset == self.boot_header.plm_source_offset):
            return self.boot_header.total_plm_length
        return default_length

    def dump_boot_header(self, filename: Optional[str] = None) -> bool:
        """Dump the boot header to a binary file (boot_bh.bin).
        
        This method creates a boot header binary file that can be used by
        the BootROM/PLM. The output format matches bootgen's dump format,
        excluding the first 16 bytes (SMAP bus width detection words) as
        these are not required by the BootROM/PLM.
        
        The output file contains the boot header from offset 0x10 to 0xF7F
        (3952 bytes total), matching the VersalBootHeaderStructure minus
        the smapWords field.
        
        Args:
            filename: Optional filename to write the boot header to.
                     If not provided, uses the input filename with "_bh.bin" suffix.
        
        Returns:
            True if the dump was successful, False otherwise.
        
        Raises:
            RuntimeError: If no image data has been parsed.
        """
        if self.boot_header is None:
            raise RuntimeError("No boot header available. Call parse() first.")
        
        # Determine the output filename
        if filename is None:
            if self.image_path is not None:
                # Remove extension and add _bh.bin suffix
                base_name = str(self.image_path)
                if '.' in base_name:
                    base_name = base_name.rsplit('.', 1)[0]
                filename = f"{base_name}_bh.bin"
            else:
                filename = "boot_bh.bin"
        
        # Get the boot header bytes (excluding SMAP bus width detection)
        boot_header_data = self.boot_header.to_bytes(include_smap_width=False)
        
        # Write to file
        with open(filename, 'wb') as output_fd:
            output_fd.write(boot_header_data)
        
        return True
    
    def dump_pmc_cdo(self, filename: Optional[str] = None) -> bool:
        """Dump the PMC CDO data to a binary file (pmc_cdo.bin).
        
        The PMC CDO (Configuration Data Object) is located immediately after
        the PLM data in the boot image. This method extracts the PMC CDO data
        based on the boot header fields.
        
        The PMC CDO is located at:
            offset = plm_source_offset + plm_length
            size = total_pmc_data_length
        
        Args:
            filename: Optional filename to write the PMC CDO to.
                     If not provided, uses "pmc_cdo.bin".
        
        Returns:
            True if the dump was successful, False otherwise.
        
        Raises:
            RuntimeError: If no image data has been parsed.
            ValueError: If the PMC CDO data is not available in the PDI.
        """
        if self.boot_header is None:
            raise RuntimeError("No boot header available. Call parse() first.")
        
        if not self.image_fd:
            raise RuntimeError("No image data loaded. Call open() first.")
        
        # Check if PMC CDO data is available
        if (self.boot_header.total_pmc_data_length is None or 
            self.boot_header.total_pmc_data_length == 0):
            raise ValueError("PMC_CDO partition is not available in the PDI")
        
        # Determine the output filename
        if filename is None:
            filename = "pmc_cdo.bin"
        
        # Calculate offset: PMC CDO is right after PLM data
        pmc_cdo_offset = self.boot_header.plm_source_offset + self.boot_header.plm_length
        pmc_cdo_length = self.boot_header.total_pmc_data_length
        
        # Seek to the PMC CDO location
        self._safe_seek(self.image_path_offset + pmc_cdo_offset, pmc_cdo_length)
        
        # Read and write in chunks
        chunk_size = 4096
        bytes_remaining = pmc_cdo_length
        with open(filename, 'wb') as output_fd:
            while bytes_remaining > 0:
                read_size = min(chunk_size, bytes_remaining)
                chunk = self.image_fd.read(read_size)
                output_fd.write(chunk)
                bytes_remaining -= read_size
        
        return True
    
    def _parse_optional_data(self) -> None:
        """Parse optional data entries from the PDI.
        
        Optional data is located immediately after the Image Header Table (IHT).
        The size is specified in the IHT's optional_data_length field (in words).
        
        Raises:
            RuntimeError: If no image data is loaded.
            ValueError: If optional data parsing fails.
        """
        if not self.image_fd:
            raise RuntimeError("No image data loaded. Call open() first.")
        
        if not self.image_header_table.optional_data_length:
            return
        
        # Calculate the starting offset of optional data
        # It's located right after the IHT
        iht_offset = self.boot_header.meta_header_offset
        optional_data_offset = iht_offset + type(self.image_header_table).IMAGE_HEADER_TABLE_SIZE
        optional_data_size_bytes = self.image_header_table.optional_data_length * 4
        
        # Seek to optional data
        self._safe_seek(self.image_path_offset + optional_data_offset, optional_data_size_bytes)
        
        offset = 0
        while offset < optional_data_size_bytes:
            opt_data_header = self.image_fd.read(4)
            if len(opt_data_header) != 4:
                print("Warning: Optional Data Header read error, unable to read 4 bytes.")
                break
            offset += 4

            header_word = struct.unpack('<I', opt_data_header)[0]
            data_id = header_word & 0xFFFF
            size_words = (header_word >> 16) & 0xFFFF
            if data_id == 0 or size_words == 0 or data_id == 0xFFFF:
                # Indicates padding or end of optional data, so we're done
                break

            # Verify entry fits within optional data region
            if offset + (size_words - 1) * 4 > optional_data_size_bytes:
                break

            opt_data = VersalOptionalData()
            opt_data.data_id = data_id
            opt_data.size_words = size_words

            if size_words == 1:
                opt_data.data = b''  # Only header + checksum
            else:
                # Size_words - 1, as the size word IS part of the size!
                opt_data.data = self.image_fd.read((size_words - 1) * 4)
                if len(opt_data.data) != ((size_words - 1) * 4):
                    print(f"Warning: Optional Data Header read error, unable to read {(size_words - 1) * 4} bytes.")
                    break
                offset += (size_words - 1) * 4

                opt_data.checksum = opt_data.data[-4:]
                opt_data.checksum = struct.unpack('<I', opt_data.checksum)[0]
                opt_data.data = opt_data.data[:-4]

            self.optional_data.append(opt_data)
    
    def list_optional_data(self) -> List[Tuple[int, str, int, bool]]:
        """List all optional data entries in the PDI.
        
        Returns:
            A list of tuples containing:
                - Index (int): The entry index
                - Data ID Name (str): Human-readable name of the data ID
                - Data Size (int): Size of the data in bytes (excluding header and checksum)
                - Checksum Valid (bool): Whether the checksum is valid
        
        Raises:
            RuntimeError: If no image data has been parsed.
        """
        if not self.image_fd:
            raise RuntimeError("No image data loaded. Call open() first.")
        
        result = []
        for idx, opt_data in enumerate(self.optional_data):
            data_id_name = opt_data.get_data_id_name()
            data_size = len(opt_data.data) if opt_data.data else 0
            checksum_valid = opt_data.verify_checksum()
            result.append((idx, data_id_name, data_size, checksum_valid))
        
        return result
    
    def extract_optional_data(self, index: int, filename: Optional[str] = None) -> bool:
        """Extract optional data entry to a file.
        
        Args:
            index: The index of the optional data entry to extract.
            filename: Optional filename to write the data to.
                     If not provided, uses format: optional_data_<id>_<index>.bin
        
        Returns:
            True if extraction was successful, False otherwise.
        
        Raises:
            RuntimeError: If no image data is loaded.
            IndexError: If the index is out of range.
        """
        if not self.image_fd:
            raise RuntimeError("No image data loaded. Call open() first.")
        
        if index < 0 or index >= len(self.optional_data):
            raise IndexError(f"Optional data index {index} out of range (0-{len(self.optional_data) - 1})")
        
        opt_data = self.optional_data[index]
        
        # Determine output filename
        if filename is None:
            data_id_name = opt_data.get_data_id_name().lower()
            filename = f"optional_data_{data_id_name}_{index}.bin"
        
        # Write data to file
        with open(filename, 'wb') as output_fd:
            if opt_data.data:
                output_fd.write(opt_data.data)
        
        return True
