# CHIPSEC: Platform Security Assessment Framework
# Copyright (c) 2025, Intel Corporation
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; Version 2.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
#
# Contact information:
# chipsec@intel.com
#


"""
Simple ACPI AML Binary Parser
Extracts OperationRegion definitions and _CRS resource descriptors from DSDT/SSDT binaries without external tools.
Supports both static OperationRegion declarations and dynamic _CRS method resource descriptors.
"""

from dataclasses import dataclass
from typing import List, Dict, Optional, Tuple
import struct


@dataclass
class OperationRegion:
    """Represents a single OperationRegion definition."""
    name: str
    space_type: int
    space_type_name: str
    base: int
    length: int
    source: str = "OperationRegion"  # Track region source for debugging


class AMLParser:
    """Simple AML binary parser for OperationRegion enumeration."""

    # AML Opcodes
    OPCODE_OPERATION_REGION = 0x5B
    SUBOPCODE_OPERATION_REGION = 0x80
    OPCODE_NAME = 0x08
    OPCODE_STORE = 0x70
    OPCODE_METHOD = 0x14
    OPCODE_RETURN = 0xA4

    # Integer encoding opcodes
    ZERO_OP = 0x00
    ONE_OP = 0x01
    BYTE_PREFIX = 0x0A
    WORD_PREFIX = 0x0B
    DWORD_PREFIX = 0x0C
    QWORD_PREFIX = 0x0E

    # Space type names
    SPACE_TYPE_NAMES = {
        0x00: "SystemMemory",
        0x01: "SystemIO",
        0x02: "PCI_Config",
        0x03: "EmbeddedControl",
        0x04: "SMBus",
        0x05: "SystemCMOS",
        0x06: "PciBarTarget",
        0x07: "IPMI",
        0x08: "GeneralPurposeIO",
        0x09: "GenericSerialBus",
        0x0A: "PCC",
        0x0B: "PRM",
        0x0C: "FFH",
        0x0D: "GSBUS_OR_CCAGO",
    }

    def __init__(self):
        self.regions: List[OperationRegion] = []
        self.names: Dict[str, int] = {}  # Symbol table for Name() declarations

    def parse(self, dsdt_and_ssdts: List[bytes]) -> List[OperationRegion]:
        """
        Parse DSDT and optional SSDTs for OperationRegion definitions.

        Args:
            dsdt_and_ssdts: List of raw ACPI table binaries (header + content)

        Returns:
            List of OperationRegion objects
        """
        self.regions = []
        self.names = {}

        # First pass: collect all Name() declarations for symbol resolution
        for table_data in dsdt_and_ssdts:
            if len(table_data) < 36:
                continue
            aml_data = table_data[36:]
            self._scan_for_names(aml_data)

        # Second pass: scan for OperationRegion definitions
        for table_data in dsdt_and_ssdts:
            if len(table_data) < 36:
                continue
            aml_data = table_data[36:]
            self._scan_for_regions(aml_data)

        # Third pass: extract CRS Store field assignments (e.g., BAS1, LEN1, LIM1)
        for table_data in dsdt_and_ssdts:
            if len(table_data) < 36:
                continue
            aml_data = table_data[36:]
            crs_executor = CRSExecutor()
            crs_fields = crs_executor.extract_crs_fields(aml_data)

            # Create synthetic OperationRegions for each BAS field found
            for field_name, value in crs_fields.items():
                if field_name.startswith('BAS'):
                    # Extract the numeric suffix (e.g., "BAS1" -> 1)
                    base_name = field_name.rstrip('\x00')
                    suffix = base_name[3:]  # Get "1" from "BAS1"

                    # Look for corresponding LEN and LIM fields
                    len_name = f'LEN{suffix}'
                    lim_name = f'LIM{suffix}'

                    length = crs_fields.get(len_name, 0)
                    limit = crs_fields.get(lim_name, 0)

                    if value > 0 and length > 0:
                        # Try to get the buffer name (PMCR, SPIR, P2BR) from mapping
                        buffer_name = crs_executor.field_to_buffer.get(base_name, '_CRS')

                        # Create synthetic region from CRS fields
                        # Name format: PMCR:BAS1 or _CRS:BAS1 if buffer not found
                        region = OperationRegion(
                            name=f'{buffer_name}:{base_name}',
                            space_type=0,  # SystemMemory
                            space_type_name='SystemMemory',
                            base=value,
                            length=length,
                            source='CRS_Fields'
                        )
                        self.regions.append(region)

        # Sort by address for presentation
        self.regions = sorted([r for r in self.regions if r.base < 0x1000000000],
                             key=lambda x: x.base)

        return self.regions

    def _scan_for_regions(self, aml_binary: bytes) -> None:
        """Scan binary for OperationRegion opcodes."""
        i = 0
        max_search = len(aml_binary)
        iter_count = 0
        max_iters = max_search * 2  # Safety: prevent infinite looping

        while i < max_search - 1 and iter_count < max_iters:
            iter_count += 1
            try:
                # Look for opcode sequence: 0x5B 0x80
                if (aml_binary[i] == self.OPCODE_OPERATION_REGION and
                    i + 1 < len(aml_binary) and
                    aml_binary[i + 1] == self.SUBOPCODE_OPERATION_REGION):

                    # Found OperationRegion opcode, try to decode
                    region = self._decode_operation_region(aml_binary, i + 2)
                    if region:
                        self.regions.append(region)
                    # Always skip forward by at least 5 bytes
                    i += 5
                else:
                    i += 1
            except Exception:
                # On any error, skip forward
                i += 5

    def _scan_for_names(self, aml_binary: bytes) -> None:
        """Scan binary for Name() declarations to build symbol table."""
        i = 0
        max_search = len(aml_binary)
        iter_count = 0
        max_iters = max_search * 2

        while i < max_search - 1 and iter_count < max_iters:
            iter_count += 1
            try:
                # Look for Name opcode: 0x08
                if aml_binary[i] == self.OPCODE_NAME:
                    # Name(name, value) - try to extract
                    name, name_len = self._decode_name_string_simple(aml_binary, i + 1)
                    if name and name_len > 0 and name_len < 20:
                        pos = i + 1 + name_len
                        if pos < len(aml_binary):
                            value, val_len = self._decode_aml_integer(aml_binary, pos)
                            if value is not None and val_len > 0:
                                # Store in symbol table
                                self.names[name] = value
                    i += 5
                else:
                    i += 1
            except Exception:
                i += 5
    def _decode_operation_region(self, aml_binary: bytes, offset: int) -> Optional[OperationRegion]:
        """
        Decode a single OperationRegion definition.

        After opcode (0x5B 0x80):
            NameString (variable length, 1-4 bytes typically)
            SpaceType (1 byte)
            Address (AML Integer, 1-9 bytes)
            Length (AML Integer, 1-9 bytes)
        """
        if offset >= len(aml_binary) or offset + 5 > len(aml_binary):
            return None

        # Extract NameString (simple: just read up to 4 bytes as NameSeg)
        name, name_len = self._decode_name_string_simple(aml_binary, offset)
        if not name or name_len <= 0 or name_len > 20:
            return None

        pos = offset + name_len
        if pos >= len(aml_binary):
            return None

        # Extract SpaceType (1 byte)
        space_type = aml_binary[pos]
        pos += 1

        if pos >= len(aml_binary):
            return None

        # Extract Address (AML Integer)
        base, int_len = self._decode_aml_integer(aml_binary, pos)
        if base is None:
            # Try to resolve as variable reference
            var_name, var_len = self._decode_name_string_simple(aml_binary, pos)
            if var_name and var_name in self.names:
                base = self.names[var_name]
                int_len = var_len
            else:
                return None

        if int_len <= 0 or int_len > 10:
            return None

        pos += int_len
        if pos >= len(aml_binary):
            return None

        # Extract Length (AML Integer)
        length, int_len = self._decode_aml_integer(aml_binary, pos)
        if length is None:
            # Try to resolve as variable reference
            var_name, var_len = self._decode_name_string_simple(aml_binary, pos)
            if var_name and var_name in self.names:
                length = self.names[var_name]
                int_len = var_len
            else:
                return None

        if int_len <= 0 or int_len > 10:
            return None

        # Sanity checks
        if length == 0 or length > 0x10000000:  # Skip regions > 256MB
            return None
        if base == 0 or base == 0xFFFFFFFF:  # Skip invalid addresses
            return None

        space_type_name = self.SPACE_TYPE_NAMES.get(space_type, f"Unknown(0x{space_type:02X})")

        return OperationRegion(
            name=name,
            space_type=space_type,
            space_type_name=space_type_name,
            base=base,
            length=length
        )

    def _decode_name_string_simple(self, aml_binary: bytes, offset: int) -> Tuple[Optional[str], int]:
        """
        Simplified name string decoder - handles most common cases.
        Returns (name_string, bytes_consumed) or (None, 0) on error.
        """
        if offset >= len(aml_binary):
            return None, 0

        byte = aml_binary[offset]

        # RootPath prefix (0x5C)
        if byte == 0x5C:
            pos = offset + 1
            # Read NameSeg after root path
            if pos + 4 <= len(aml_binary):
                seg = self._read_name_seg(aml_binary, pos)
                if seg:
                    return '\\' + seg, 5
            return '\\', 1

        # ParentPath prefix (0x5E) - skip for now, just return None
        if byte == 0x5E:
            return None, 0

        # DualNamePath (0x2E)
        if byte == 0x2E:
            pos = offset + 1
            parts = []
            for _ in range(2):
                if pos + 4 <= len(aml_binary):
                    seg = self._read_name_seg(aml_binary, pos)
                    if seg:
                        parts.append(seg)
                        pos += 4
                    else:
                        break
            if len(parts) == 2:
                return '.'.join(parts), pos - offset
            return None, 0

        # MultiNamePath (0x2F)
        if byte == 0x2F:
            pos = offset + 1
            if pos >= len(aml_binary):
                return None, 0
            count = aml_binary[pos]
            pos += 1
            if count > 20 or count == 0:  # Sanity check
                return None, 0
            parts = []
            for _ in range(count):
                if pos + 4 <= len(aml_binary):
                    seg = self._read_name_seg(aml_binary, pos)
                    if seg:
                        parts.append(seg)
                        pos += 4
                    else:
                        break
            if len(parts) == count:
                return '.'.join(parts), pos - offset
            return None, 0

        # Simple NameSeg (4 characters: A-Z, 0-9, _)
        if self._is_name_char(byte):
            if offset + 4 <= len(aml_binary):
                seg = self._read_name_seg(aml_binary, offset)
                if seg:
                    return seg, 4

        return None, 0

    def _read_name_seg(self, aml_binary: bytes, offset: int) -> Optional[str]:
        """
        Read exactly 4 bytes as a NameSeg.
        Valid characters: A-Z, a-z, 0-9, _
        """
        if offset + 4 > len(aml_binary):
            return None

        seg = b''
        for i in range(4):
            byte = aml_binary[offset + i]
            if self._is_name_char(byte):
                seg += bytes([byte])
            elif byte == 0x5F:  # _ padding
                seg += bytes([byte])
            else:
                # Invalid character in NameSeg
                return None

        # Decode and strip trailing underscores
        name = seg.decode('ascii', errors='ignore').rstrip('_')
        return name if name else None

    def _is_name_char(self, byte: int) -> bool:
        """Check if byte is valid in a name (A-Z, a-z, 0-9, _)."""
        return ((ord('A') <= byte <= ord('Z')) or
                (ord('a') <= byte <= ord('z')) or
                (ord('0') <= byte <= ord('9')) or
                byte == ord('_'))

    def _decode_aml_integer(self, aml_binary: bytes, offset: int) -> Tuple[Optional[int], int]:
        """
        Decode AML Integer (variable-length encoding).
        Returns (value, bytes_consumed) or (None, 0) if variable reference detected.

        Formats:
            0x00 = zero (1 byte)
            0x01 = one (1 byte)
            0x0A = byte value follows (2 bytes total)
            0x0B = word value follows (3 bytes total)
            0x0C = dword value follows (5 bytes total)
            0x0E = qword value follows (9 bytes total)
            0x02-0x09 = literal single-byte values
        """
        if offset >= len(aml_binary):
            return None, 0

        opcode = aml_binary[offset]

        # Zero and one
        if opcode == self.ZERO_OP:
            return 0, 1
        elif opcode == self.ONE_OP:
            return 1, 1

        # Prefixed integers
        elif opcode == self.BYTE_PREFIX:
            if offset + 2 <= len(aml_binary):
                return aml_binary[offset + 1], 2
        elif opcode == self.WORD_PREFIX:
            if offset + 3 <= len(aml_binary):
                return struct.unpack('<H', aml_binary[offset + 1:offset + 3])[0], 3
        elif opcode == self.DWORD_PREFIX:
            if offset + 5 <= len(aml_binary):
                return struct.unpack('<I', aml_binary[offset + 1:offset + 5])[0], 5
        elif opcode == self.QWORD_PREFIX:
            if offset + 9 <= len(aml_binary):
                return struct.unpack('<Q', aml_binary[offset + 1:offset + 9])[0], 9

        # Direct single-byte values (0x02-0x09)
        elif 0x02 <= opcode <= 0x09:
            return opcode, 1

        # If it looks like a NameSeg or path, it's a variable reference
        # Return None to indicate this can't be decoded statically
        elif self._is_name_char(opcode) or opcode in (0x5C, 0x5E, 0x2E, 0x2F):
            return None, 0

        return None, 0


class ResourceDescriptorParser:
    """Parse resource descriptors in _CRS methods."""

    # Large resource descriptor opcodes (bit 7 = 1)
    DWORD_MEMORY = 0x87      # DWordMemory
    QWORD_MEMORY = 0x8A      # QWordMemory
    DWORD_IO = 0x81          # DWordIO
    QWORD_IO = 0x8B          # QWordIO
    DWORD_SPACE = 0x8C       # DWordSpace
    QWORD_SPACE = 0x8E       # QWordSpace

    def __init__(self):
        self.regions: List[Dict] = []

    def extract_from_crs_binary(self, aml_binary: bytes, field_values: Dict[str, int]) -> List[Dict]:
        """
        Scan _CRS method binary for resource descriptors and extract memory regions.
        field_values: dictionary of Store assignments (BAS1=0x..., LEN1=0x..., etc.)
        """
        regions = []
        i = 0

        while i < len(aml_binary) - 2:
            opcode = aml_binary[i]

            # Large resource items have bit 7 set (0x80-0xFF)
            if opcode >= 0x80:
                try:
                    # Large resource format: opcode, length_low, length_high, data...
                    if i + 2 >= len(aml_binary):
                        i += 1
                        continue

                    length = aml_binary[i + 1] | (aml_binary[i + 2] << 8)
                    data_start = i + 3
                    data_end = data_start + length

                    if data_end > len(aml_binary):
                        i += 1
                        continue

                    # Parse based on opcode
                    if opcode in (self.DWORD_MEMORY, self.QWORD_MEMORY,
                                  self.DWORD_IO, self.QWORD_IO,
                                  self.DWORD_SPACE, self.QWORD_SPACE):
                        region = self._parse_address_space(opcode, aml_binary[data_start:data_end], field_values)
                        if region:
                            regions.append(region)

                    i = data_end
                except Exception:
                    i += 1
            else:
                i += 1

        return regions

    def _parse_address_space(self, opcode: int, data: bytes, field_values: Dict[str, int]) -> Optional[Dict]:
        """Parse DWordMemory, QWordMemory, etc. resource descriptor."""
        if len(data) < 1:
            return None

        # Skip resource type specific parts, look for address/length
        # Format varies, but generally: flags, type_specific, then min/max/length

        try:
            if opcode in (self.DWORD_MEMORY, self.DWORD_IO, self.DWORD_SPACE):
                # DWord format
                if len(data) >= 20:
                    # Typical: flags(1), type(1), ... min(4), max(4), len(4)
                    # This is simplified - actual format varies by type
                    base = struct.unpack('<I', data[10:14])[0] if len(data) >= 14 else None
                    size = struct.unpack('<I', data[16:20])[0] if len(data) >= 20 else None

                    if base and size and size > 0 and size < 0x10000000:
                        return {
                            'name': f'MEM_0x{base:08X}',
                            'base': base,
                            'size': size
                        }

            elif opcode in (self.QWORD_MEMORY, self.QWORD_IO, self.QWORD_SPACE):
                # QWord format
                if len(data) >= 36:
                    # Similar structure but 64-bit addresses
                    base = struct.unpack('<Q', data[14:22])[0] if len(data) >= 22 else None
                    size = struct.unpack('<Q', data[28:36])[0] if len(data) >= 36 else None

                    if base and size and size > 0 and size < 0x10000000000:
                        return {
                            'name': f'MEM_0x{base:016X}',
                            'base': base,
                            'size': size
                        }
        except Exception:
            pass

        return None


class CRSExecutor:
    """Simple extractor for field assignments in _CRS methods."""

    OPCODE_STORE = 0x70
    OPCODE_METHOD = 0x14
    OPCODE_DEVICE = 0x82
    OPCODE_SCOPE = 0x10
    OPCODE_CREATE_DWORD_FIELD = 0x8A

    def __init__(self):
        self.field_values: Dict[str, int] = {}
        self.field_to_buffer: Dict[str, str] = {}  # Maps BAS1 -> PMCR, etc.
        self.context_method: str = "_CRS"  # Default to _CRS method name

    def extract_crs_fields(self, aml_binary: bytes) -> Dict[str, int]:
        """
        Scan for Store opcode patterns targeting BAS/LEN/LIM field assignments.
        Also maps field names to their source buffer names via CreateDWordField.
        """
        try:
            # First pass: build field-to-buffer mapping from CreateDWordField
            self._map_fields_to_buffers(aml_binary)

            # Second pass: scan for Store operations
            i = 0
            iterations = 0
            max_iter_limit = 1000000  # Hard stop after 1M iterations

            while i < len(aml_binary) - 5 and iterations < max_iter_limit:
                iterations += 1

                if aml_binary[i] == self.OPCODE_STORE:
                    try:
                        # Decode value following Store opcode
                        value, val_len = self._decode_simple_int(aml_binary, i + 1)

                        if value is not None and 0 < val_len < 10:
                            # Check for valid name at target position
                            name_pos = i + 1 + val_len
                            if name_pos + 4 <= len(aml_binary):
                                # Extract 4-byte name
                                name_bytes = aml_binary[name_pos:name_pos+4]

                                # Check if bytes are ASCII-like
                                try:
                                    name = name_bytes.decode('ascii')
                                    # Keep only BAS, LEN, LIM patterns
                                    if any(name.startswith(p) for p in ['BAS', 'LEN', 'LIM']):
                                        clean_name = name.rstrip('\x00')
                                        if clean_name and len(clean_name) >= 4:
                                            self.field_values[clean_name] = value
                                except:
                                    pass
                    except:
                        pass

                i += 1
        except:
            pass

        return self.field_values

    def _map_fields_to_buffers(self, aml_binary: bytes) -> None:
        """Build mapping of field names to buffer names via CreateDWordField opcodes."""
        try:
            i = 0
            while i < len(aml_binary) - 10:
                if aml_binary[i] == self.OPCODE_CREATE_DWORD_FIELD:
                    try:
                        # After CreateDWordField opcode comes:
                        # SourceBuffer (Name), ByteIndex (Integer), FieldName (Name)

                        # Try to extract buffer name (usually 4 bytes)
                        pos = i + 1
                        if pos + 4 <= len(aml_binary):
                            # Extract potential buffer name (4 bytes)
                            buf_bytes = aml_binary[pos:pos+4]
                            try:
                                buf_name = buf_bytes.decode('ascii').rstrip('\x00')
                                if buf_name and len(buf_name) >= 3 and buf_name[0] not in '\x00\x01\x02':
                                    # Skip the buffer name and what follows (typically variable-length)
                                    # Try to find field name (BAS, LEN, LIM) in nearby bytes
                                    for j in range(pos, min(pos + 20, len(aml_binary) - 4)):
                                        field_bytes = aml_binary[j:j+4]
                                        try:
                                            field_name = field_bytes.decode('ascii').rstrip('\x00')
                                            if field_name and any(field_name.startswith(p) for p in ['BAS', 'LEN', 'LIM']):
                                                # Found a field name, map it to buffer
                                                self.field_to_buffer[field_name] = buf_name
                                                break
                                        except:
                                            pass
                            except:
                                pass
                    except:
                        pass
                i += 1
        except:
            pass

    def _find_crs_method_context(self, aml_binary: bytes) -> None:
        """Scan for _CRS method name for context."""
        try:
            # Look for pattern: Method opcode (0x14) followed by name string "_CRS"
            i = 0
            while i < len(aml_binary) - 10:
                if aml_binary[i] == self.OPCODE_METHOD:
                    # After Method opcode comes name (typically "_CRS" = 0x5F 0x43 0x52 0x53)
                    if i + 5 < len(aml_binary):
                        potential_name = aml_binary[i+1:i+5]
                        if b'_CRS' in potential_name or b'_CRS' == potential_name:
                            self.context_method = "_CRS"
                            return
                i += 1
        except:
            pass

    def _decode_simple_int(self, data: bytes, offset: int) -> Tuple[Optional[int], int]:
        """Decode integer at offset."""
        if offset >= len(data):
            return None, 0

        op = data[offset]
        if op == 0x00:
            return 0, 1
        elif op == 0x01:
            return 1, 1
        elif op == 0x0A and offset + 2 <= len(data):  # Byte
            return data[offset + 1], 2
        elif op == 0x0B and offset + 3 <= len(data):  # Word
            return struct.unpack('<H', data[offset + 1:offset + 3])[0], 3
        elif op == 0x0C and offset + 5 <= len(data):  # DWord
            return struct.unpack('<I', data[offset + 1:offset + 5])[0], 5
        elif op == 0x0E and offset + 9 <= len(data):  # QWord
            return struct.unpack('<Q', data[offset + 1:offset + 9])[0], 9
        elif 0x02 <= op <= 0x09:
            return op, 1

        return None, 0

    def _decode_simple_name(self, data: bytes, offset: int) -> Tuple[Optional[str], int]:
        """Decode a simple 4-byte name segment."""
        if offset + 4 > len(data):
            return None, 0

        seg = data[offset:offset+4]
        # Check if all bytes are valid name chars
        for b in seg:
            if not ((ord('A') <= b <= ord('Z')) or (ord('a') <= b <= ord('z')) or
                    (ord('0') <= b <= ord('9')) or b == ord('_')):
                return None, 0

        name = seg.decode('ascii', errors='ignore').rstrip('_')
        return name if name else None, 4


class CRSResourceParser:
    """
    Extracts and decodes _CRS method resource descriptors from AML binary.
    Handles both static _CRS methods with direct Buffer returns and dynamic field assignments.
    """

    @staticmethod
    def parse_pkg_length(aml: bytes, pos: int) -> Tuple[int, int]:
        """
        Parse AML package length (variable-size encoding).

        Returns:
            (length, new_pos)
        """
        if pos >= len(aml):
            return 0, pos

        lead = aml[pos]
        pos += 1

        if lead < 0x40:
            return lead, pos

        byte_count = (lead >> 6) & 0x03
        length = lead & 0x0F

        for i in range(byte_count):
            if pos >= len(aml):
                return length, pos
            length |= (aml[pos] << (4 + 8 * i))
            pos += 1

        return length, pos

    @staticmethod
    def extract_crs_buffer(aml_bytes: bytes, target_name: bytes = b'_CRS') -> Optional[bytes]:
        """
        Walk AML binary to find _CRS method and extract its Buffer if simple Return(Buffer).

        Args:
            aml_bytes: Raw AML binary content
            target_name: Method name to search for (e.g., b'_CRS')

        Returns:
            Raw buffer bytes if found and parseable, None otherwise
        """
        pos = 0
        while pos < len(aml_bytes):
            try:
                opcode = aml_bytes[pos]

                if opcode == 0x14:  # MethodOp
                    pos += 1
                    pkg_len, pos = CRSResourceParser.parse_pkg_length(aml_bytes, pos)
                    end_pos = pos + pkg_len

                    # NameString (4 bytes for simple name, or 0x00 0x00 for multi-segment)
                    if pos + 4 > len(aml_bytes):
                        pos = end_pos
                        continue

                    name = aml_bytes[pos:pos+4]
                    pos += 4

                    # Check if this is our target _CRS method
                    is_match = False
                    if target_name == b'_CRS' and name[:4] == b'_CRS':
                        is_match = True
                    elif name == target_name + b'\x00' * (4 - len(target_name)):
                        is_match = True

                    if is_match:
                        # Skip flags byte
                        if pos >= len(aml_bytes):
                            pos = end_pos
                            continue
                        pos += 1

                        # Look for ReturnOp (0xA4)
                        while pos < end_pos and pos < len(aml_bytes):
                            if aml_bytes[pos] == 0xA4:  # ReturnOp
                                pos += 1

                                # Check for BufferOp (0x11)
                                if pos >= len(aml_bytes):
                                    break
                                if aml_bytes[pos] == 0x11:
                                    pos += 1
                                    buf_pkg_len, pos = CRSResourceParser.parse_pkg_length(aml_bytes, pos)

                                    # Parse BufferSize (assume simple ByteConst/WordConst)
                                    if pos >= len(aml_bytes):
                                        break

                                    if aml_bytes[pos] == 0x0A:  # ByteConst
                                        pos += 1
                                        if pos >= len(aml_bytes):
                                            break
                                        buf_size = aml_bytes[pos]
                                        pos += 1
                                    elif aml_bytes[pos] == 0x0B:  # WordConst
                                        pos += 1
                                        if pos + 2 > len(aml_bytes):
                                            break
                                        buf_size = struct.unpack('<H', aml_bytes[pos:pos+2])[0]
                                        pos += 2
                                    elif aml_bytes[pos] == 0x0C:  # DWordConst
                                        pos += 1
                                        if pos + 4 > len(aml_bytes):
                                            break
                                        buf_size = struct.unpack('<I', aml_bytes[pos:pos+4])[0]
                                        pos += 4
                                    else:
                                        break

                                    # Extract ByteList
                                    if pos + buf_size > len(aml_bytes):
                                        buf_size = len(aml_bytes) - pos

                                    buffer_data = aml_bytes[pos:pos + buf_size]
                                    return buffer_data
                                break
                            pos += 1

                    pos = end_pos
                else:
                    pos += 1
            except Exception:
                pos += 1

        return None

    @staticmethod
    def decode_crs_buffer(buffer_bytes: bytes) -> List[Dict]:
        """
        Decode resource descriptors from _CRS Buffer bytes.

        Args:
            buffer_bytes: Raw resource descriptor buffer

        Returns:
            List of dicts: {type, description, address, size} for memory/IO resources
        """
        pos = 0
        resources = []

        while pos < len(buffer_bytes):
            if pos >= len(buffer_bytes):
                break

            tag = buffer_bytes[pos]

            # Check for End Tag (0x79)
            if tag == 0x79:
                if pos + 1 < len(buffer_bytes):
                    checksum = buffer_bytes[pos + 1]
                    # Optional: verify checksum
                break

            try:
                if tag & 0x80 == 0:  # Small descriptor
                    item_type = (tag >> 3) & 0x0F
                    length = tag & 0x07
                    data = buffer_bytes[pos + 1:pos + 1 + length]
                    pos += 1 + length
                else:  # Large descriptor
                    item_type = tag
                    if pos + 3 > len(buffer_bytes):
                        break
                    length = struct.unpack_from('<H', buffer_bytes, pos + 1)[0]
                    data = buffer_bytes[pos + 3:pos + 3 + length]
                    pos += 3 + length

                # Decode specific descriptor type
                res_dict = CRSResourceParser._decode_descriptor(item_type, data)
                if res_dict:
                    resources.append(res_dict)
            except Exception:
                pos += 1

        return resources

    @staticmethod
    def _decode_descriptor(item_type: int, data: bytes) -> Optional[Dict]:
        """Decode a single resource descriptor and extract address/size for memory resources."""

        try:
            # 0x84: 32-bit Memory Range (large)
            if item_type == 0x84:
                if len(data) >= 9:
                    flags = data[0]
                    min_addr = struct.unpack('<I', data[1:5])[0]
                    max_addr = struct.unpack('<I', data[5:9])[0]
                    alignment = struct.unpack('<I', data[9:13])[0] if len(data) >= 13 else 0
                    length = struct.unpack('<I', data[13:17])[0] if len(data) >= 17 else 0

                    return {
                        'type': 'Memory32',
                        'address': min_addr,
                        'size': length if length > 0 else (max_addr - min_addr + 1),
                        'flags': flags,
                        'description': f'Memory32: base=0x{min_addr:08x}, size=0x{length:08x}'
                    }

            # 0x85: 32-bit Fixed Memory (large)
            elif item_type == 0x85:
                if len(data) >= 9:
                    flags = data[0]
                    base = struct.unpack('<I', data[1:5])[0]
                    length = struct.unpack('<I', data[5:9])[0]

                    return {
                        'type': 'FixedMemory32',
                        'address': base,
                        'size': length,
                        'flags': flags,
                        'description': f'FixedMemory32: base=0x{base:08x}, size=0x{length:08x}'
                    }

            # 0x87: DWord Address Space (large) - Memory or I/O
            elif item_type == 0x87:
                if len(data) >= 23:
                    res_type = data[0]
                    gen_flags = data[1]
                    spec_flags = data[2]
                    min_addr = struct.unpack('<I', data[4:8])[0]
                    max_addr = struct.unpack('<I', data[8:12])[0]
                    gran = struct.unpack('<I', data[12:16])[0]
                    trans = struct.unpack('<I', data[16:20])[0]
                    length = struct.unpack('<I', data[20:24])[0]

                    type_names = {0: 'Memory', 1: 'I/O', 2: 'BusNumber', 3: 'Reserved'}
                    type_str = type_names.get(res_type, 'Unknown')

                    if res_type == 0:  # Memory
                        return {
                            'type': f'DWord{type_str}',
                            'address': min_addr,
                            'size': length if length > 0 else (max_addr - min_addr + 1),
                            'flags': gen_flags,
                            'description': f'DWord Memory: base=0x{min_addr:08x}, size=0x{length:08x}'
                        }
                    elif res_type == 1:  # I/O
                        return {
                            'type': f'DWord{type_str}',
                            'address': min_addr,
                            'size': length if length > 0 else (max_addr - min_addr + 1),
                            'flags': gen_flags,
                            'description': f'DWord I/O: base=0x{min_addr:04x}, size=0x{length:04x}'
                        }

            # 0x8A: QWord Address Space (large)
            elif item_type == 0x8A:
                if len(data) >= 47:
                    res_type = data[0]
                    gen_flags = data[1]
                    spec_flags = data[2]
                    min_addr = struct.unpack('<Q', data[4:12])[0]
                    max_addr = struct.unpack('<Q', data[12:20])[0]
                    gran = struct.unpack('<Q', data[20:28])[0]
                    trans = struct.unpack('<Q', data[28:36])[0]
                    length = struct.unpack('<Q', data[36:44])[0]

                    type_names = {0: 'Memory', 1: 'I/O', 2: 'BusNumber', 3: 'Reserved'}
                    type_str = type_names.get(res_type, 'Unknown')

                    if res_type == 0:  # Memory
                        return {
                            'type': f'QWord{type_str}',
                            'address': min_addr,
                            'size': length if length > 0 else (max_addr - min_addr + 1),
                            'flags': gen_flags,
                            'description': f'QWord Memory: base=0x{min_addr:016x}, size=0x{length:016x}'
                        }

            # 0x04: IRQ (small) - included for completeness
            elif item_type == 0x04:
                if len(data) >= 2:
                    flags = data[0]
                    irq_mask = struct.unpack('<H', data[1:3])[0] if len(data) >= 3 else data[1]
                    return {
                        'type': 'IRQ',
                        'description': f'IRQ: mask=0x{irq_mask:04x}'
                    }

            # 0x05: DMA (small)
            elif item_type == 0x05:
                if len(data) >= 2:
                    flags = data[0]
                    chan_mask = data[1]
                    return {
                        'type': 'DMA',
                        'description': f'DMA: channels=0x{chan_mask:02x}'
                    }

        except Exception:
            pass

        return None


def parse_operation_regions(tables: List[bytes]) -> List[Dict]:
    """
    Parse DSDT and SSDTs for all OperationRegion definitions.

    Args:
        tables: List of raw ACPI table binaries (header + AML content)

    Returns:
        List of dicts: {name, space_type, space_type_name, base, length}
    """
    parser = AMLParser()
    regions = parser.parse(tables)

    return [
        {
            'name': r.name,
            'space_type': r.space_type,
            'space_type_name': r.space_type_name,
            'base': r.base,
            'length': r.length,
            'source': r.source
        }
        for r in regions
    ]
