"""
ResourceParser — zero-dependency resources.arsc string table resolver.

Parses just enough of the binary resources.arsc format to resolve a
resource ID (e.g. 0x7F0C0012) to its default-locale string value and
to its key name (e.g. app_name, hello).

resources.arsc binary layout (simplified):
  RES_TABLE_TYPE         chunk header (type=0x0002)
    StringPool           global value string pool (type=0x0001)
    RES_TABLE_PACKAGE    one per package (type=0x0200)
      StringPool         type string pool
      StringPool         key string pool
      RES_TABLE_TYPE_TYPE  per-type entry table (type=0x0201)
        ResTable_entry   size(2) flags(2) key(4) [key=index into key pool]
        Res_value       ...

We need:
  1. The global value StringPool to get all string values
  2. Each package's key string pool for entry names
  3. Each RES_TABLE_TYPE_TYPE chunk: res_id -> value index, res_id -> key index
"""
from __future__ import annotations
import struct
from typing import Optional

_RES_STRING_POOL_TYPE   = 0x0001
_RES_TABLE_TYPE         = 0x0002
_RES_TABLE_PACKAGE_TYPE = 0x0200
_RES_TABLE_TYPE_TYPE    = 0x0201


def _u8(data: bytes, off: int) -> int:
    return data[off]

def _u16(data: bytes, off: int) -> int:
    return struct.unpack_from("<H", data, off)[0]

def _u32(data: bytes, off: int) -> int:
    return struct.unpack_from("<I", data, off)[0]


def _read_string_pool(data: bytes, base: int) -> list[str]:
    """Parse a RES_STRING_POOL chunk at `base`, return list of strings."""
    strings: list[str] = []
    try:
        header_size   = _u32(data, base + 4)
        string_count  = _u32(data, base + 12)
        flags         = _u32(data, base + 20)
        strings_start = _u32(data, base + 24)
        is_utf8 = bool(flags & (1 << 8))

        offsets_base  = base + header_size
        str_data_base = base + strings_start

        for i in range(string_count):
            off = offsets_base + i * 4
            if off + 4 > len(data):
                break
            str_off = _u32(data, off)
            pos = str_data_base + str_off
            if pos >= len(data):
                strings.append("")
                continue
            try:
                if is_utf8:
                    # skip char-length (1 or 2 bytes)
                    char_len = _u8(data, pos); pos += 1
                    if char_len & 0x80: pos += 1
                    # byte-length (1 or 2 bytes)
                    byte_len = _u8(data, pos); pos += 1
                    if byte_len & 0x80:
                        byte_len = ((byte_len & 0x7F) << 8) | _u8(data, pos); pos += 1
                    s = data[pos:pos + byte_len].decode("utf-8", errors="replace")
                else:
                    # UTF-16LE
                    char_len = _u16(data, pos); pos += 2
                    if char_len & 0x8000:
                        char_len = ((char_len & 0x7FFF) << 16) | _u16(data, pos); pos += 2
                    s = data[pos:pos + char_len * 2].decode("utf-16-le", errors="replace")
                strings.append(s)
            except Exception:
                strings.append("")
    except Exception:
        pass
    return strings


class ResourceParser:
    """
    Resolve a resource ID from raw `resources.arsc` bytes.

    Usage:
        parser = ResourceParser(arsc_bytes)
        value = parser.resolve(0x7F0C0012)      # -> "My App" or None
        key_name = parser.resolve_name(0x7F0C0012)  # -> "app_name" or None
    """

    def __init__(self, arsc_bytes: bytes):
        self._data = arsc_bytes
        self._global_strings: list[str] = []
        self._res_map: dict[int, int] = {}   # res_id -> string pool index
        self._name_map: dict[int, str] = {}  # res_id -> key name (e.g. app_name)
        self._parsed = False

    def resolve(self, res_id: int) -> Optional[str]:
        """Return the default-locale string for `res_id`, or None."""
        if not self._parsed:
            self._parse()
        idx = self._res_map.get(res_id)
        if idx is None:
            return None
        if 0 <= idx < len(self._global_strings):
            val = self._global_strings[idx]
            return val if val else None
        return None

    def resolve_name(self, res_id: int) -> Optional[str]:
        """Return the key name for `res_id` (e.g. app_name, hello), or None."""
        if not self._parsed:
            self._parse()
        return self._name_map.get(res_id)

    def get_string_names(self) -> list[str]:
        """Return all string resource key names (e.g. app_name, hello) from arsc."""
        if not self._parsed:
            self._parse()
        return sorted(set(self._name_map.values()))

    def has_string_resources(self) -> bool:
        """Return True if arsc contains any string-type resources (even without key names)."""
        if not self._parsed:
            self._parse()
        return len(self._res_map) > 0

    def _parse(self):
        self._parsed = True
        data = self._data
        if len(data) < 12:
            return
        if _u16(data, 0) != _RES_TABLE_TYPE:
            return
        header_size = _u32(data, 4)
        pos = header_size
        while pos + 12 <= len(data):
            c_type = _u16(data, pos)
            # Chunk size: standard ResChunk at offset 4, some formats at offset 8
            c_size = _u32(data, pos + 4)
            if c_size < _u32(data, pos + 8):
                c_size = _u32(data, pos + 8)
            if c_size == 0:
                break
            if c_type == _RES_STRING_POOL_TYPE and not self._global_strings:
                self._global_strings = _read_string_pool(data, pos)
            elif c_type == _RES_TABLE_PACKAGE_TYPE:
                self._parse_package(pos, pos + c_size)
            pos += c_size

    def _parse_package(self, base: int, end: int):
        data = self._data
        pkg_id = _u32(data, base + 12)
        pkg_header_size = _u32(data, base + 4)
        if pkg_header_size >= (end - base) and _u16(data, base + 2) >= 8:
            pkg_header_size = _u16(data, base + 2)
        pos = base + pkg_header_size
        type_strings: list[str] = []
        key_strings: list[str] = []
        while pos + 12 <= end and pos + 12 <= len(data):
            c_type = _u16(data, pos)
            c_size = _u32(data, pos + 4)
            if c_size < _u32(data, pos + 8):
                c_size = _u32(data, pos + 8)
            if c_size == 0:
                break
            if c_type == _RES_STRING_POOL_TYPE:
                if not type_strings:
                    type_strings = _read_string_pool(data, pos)
                elif not key_strings:
                    key_strings = _read_string_pool(data, pos)
            elif c_type == _RES_TABLE_TYPE_TYPE:
                type_id = _u8(data, pos + 12)   # 1-based
                self._parse_type_chunk(pos, pkg_id, type_id, key_strings)
            pos += c_size

    def _parse_type_chunk(self, base: int, pkg_id: int, type_id: int,
                         key_strings: list[str]):
        data = self._data
        try:
            header_size = _u32(data, base + 4)
            entry_count  = _u32(data, base + 16)
            entries_start = _u32(data, base + 20)
            # Offsets array is at header_size; entry data at entries_start
            offsets_base = base + header_size
            data_base    = base + entries_start

            for i in range(entry_count):
                op = offsets_base + i * 4
                if op + 4 > len(data):
                    break
                entry_off = _u32(data, op)
                if entry_off == 0xFFFFFFFF:
                    continue

                ep = data_base + entry_off
                if ep + 4 > len(data):
                    continue
                entry_size = _u16(data, ep)
                entry_flags = _u16(data, ep + 2)
                if entry_flags & 0x0001:        # complex/bag entry
                    continue

                # ResTable_entry: size(2) flags(2) [key(4) if size>=8]
                # Res_value follows at ep+8 (full entry) or ep+4 (minimal)
                has_key = entry_size >= 8
                val_off = ep + 8 if has_key else ep + 4
                if val_off + 8 > len(data):
                    continue
                data_type = _u8(data, val_off + 3)
                val_data  = _u32(data, val_off + 4)

                if data_type == 0x03:           # TYPE_STRING
                    res_id = (pkg_id << 24) | (type_id << 16) | i
                    self._res_map[res_id] = val_data
                    if has_key and key_strings:
                        key_idx = _u32(data, ep + 4)
                        if 0 <= key_idx < len(key_strings):
                            key_name = key_strings[key_idx]
                            if key_name:
                                self._name_map[res_id] = key_name
        except Exception:
            pass
