#!/usr/bin/env python
"""
@file hci_property.py

@brief Unified HCI property system with type safety and minimal code

Copyright (C) Atmosic 2025
"""
from __future__ import annotations
from typing import TypeVar, Generic, Type, Any, Optional, Union, Callable
import struct

from .hci_packet_base import HciPacketBase

T = TypeVar('T')
# Type that represents T or None when payload is not available
OptionalT = Union[T, None]


class HciProperty(Generic[T]):
    """Unified HCI property descriptor with type safety and flexible positioning

    Returns Optional[T] - None when payload is not available, T when it is.
    """

    def __init__(self, start: int | Callable[[HciPacketBase], int], fmt: str,
                 expected: Optional[T] = None,
                 end: Optional[int| Callable[[HciPacketBase], int]] = None,
                 on_change: Optional[Callable[[HciPacketBase], None]] = None):
        """
        Initialize HCI property

        Args:
            start: Starting byte offset in payload
            fmt: struct format string (e.g., '<B', 'H', 's')
            expected: Expected/reference value for documentation (not used as default)
            end: Optional ending byte offset (for variable length fields)
            on_change: Optional callback function called when value changes (receives instance)
        """
        self.start = start
        self.end = end
        self.fmt = fmt
        self.expected = expected  # For documentation/reference only
        self.on_change = on_change
        self.name = None
        self.private_name = None
        
        # Calculate size for fixed-length fields
        if fmt != 's' and not fmt.endswith('s'):
            try:
                self.size = struct.calcsize(fmt)
            except struct.error:
                self.size = 1  # Fallback
            # Only set end for static positive start positions
            if self.end is None and isinstance(self.start, int) and self.start >= 0:
                self.end = self.start + self.size
        else:
            self.size = None  # Variable length
    
    def __set_name__(self, owner: Type, name: str):
        """Called when descriptor is assigned to a class attribute"""
        self.name = name
        self.private_name = f'_{name}'
    
    def __get__(self, instance: Any, owner: Optional[Type] = None) -> Optional[T]:
        """Get property value from instance - returns None if payload not available"""
        if instance is None:
            return self  # type: ignore
        
        assert isinstance(instance, HciPacketBase), \
            f"Expected HciPacket, got {type(instance)}"

        # Ensure payload exists
        if not instance.payload:
            return None

        # Handle negative start positions (e.g., -1 for last byte)
        start_pos = self._resolve_position(self.start, instance)
        if start_pos is None or len(instance.payload) <= start_pos:
            return None

        # Try to read from payload
        try:
            if self.fmt == 's' or self.fmt.endswith('s'):
                ret = self._get_bytes_field(instance)
                assert isinstance(ret, bytes)
                return ret
            else:
                return self._get_fixed_field(instance)
        except (IndexError, struct.error):
            return None

    def _resolve_position(self, position: Union[int, Callable],
                          instance: HciPacketBase) -> Optional[int]:
        """Resolve position (handle callables and static positions)"""
        if callable(position):
            try:
                pos = position(instance)
                return pos if pos >= 0 else None
            except (AttributeError, TypeError):
                return None
        else:
            return position if position >= 0 else None

    def __set__(self, instance: Any, value: T):
        """Set property value on instance - auto-extends payload if needed"""
        # Type hint for better IDE support (assume instance is HciPacket)
        assert isinstance(instance, HciPacketBase), \
            f"Expected HciPacketBase, got {type(instance)}"

        # Auto-extend payload if needed
        if self.fmt == 's' or self.fmt.endswith('s'):
            assert isinstance(value, bytes), \
                f"Expected bytes, got {type(value)}"
            self._set_bytes_field(instance, value)
        else:
            self._set_fixed_field(instance, value)

        # Auto-update valid_payload_size to match actual payload size
        if hasattr(instance, 'adjust_payload_size'):
            instance.adjust_payload_size(len(instance.payload))

        # Trigger on_change callback if defined
        if self.on_change is not None:
            try:
                self.on_change(instance)
            except Exception:
                # Silently ignore callback errors to prevent breaking the setter
                pass
    
    def _get_bytes_field(self, instance: HciPacketBase) -> Optional[bytes]:
        """Get variable-length bytes field"""
        payload = instance.payload
        start_pos = self._resolve_position(self.start, instance)
        if start_pos is None:
            return None

        if self.end is not None:
            # Calculate end position (support callable end and negative indices)
            end_pos = self._resolve_position(self.end, instance)
            if end_pos is not None and len(payload) >= end_pos:
                return bytes(payload[start_pos:end_pos])
            else:
                return None
        else:
            # From start to end of payload
            if len(payload) > start_pos:
                return bytes(payload[start_pos:])
            else:
                return None
    
    def _get_fixed_field(self, instance: HciPacketBase) -> Optional[T]:
        """Get fixed-size field"""
        payload = instance.payload
        start_pos = self._resolve_position(self.start, instance)
        if start_pos is None:
            return None

        if self.end is not None:
            end_pos = self._resolve_position(self.end, instance)
            if end_pos is None:
                return None
        else:
            end_pos = start_pos + (self.size or 1)

        if len(payload) >= end_pos:
            data = payload[start_pos:end_pos]
            try:
                return struct.unpack(self.fmt, data)[0]
            except struct.error:
                return None

        return None
    
    def _set_bytes_field(self, instance: HciPacketBase, value: bytes):
        """Set variable-length bytes field"""
        payload = instance.payload
        start_pos = self._resolve_position(self.start, instance)
        if start_pos is None:
            return

        if self.end is not None:
            # Fixed range - pad or truncate as needed (support callable end)
            end_pos = self._resolve_position(self.end, instance)
            if end_pos is None:
                return
            field_size = end_pos - start_pos
            if len(value) > field_size:
                value = value[:field_size]
            elif len(value) < field_size:
                value = value + b'\x00' * (field_size - len(value))

            # Ensure payload is large enough
            if len(payload) < end_pos:
                payload.extend(b'\x00' * (end_pos - len(payload)))

            payload[start_pos:end_pos] = value
        else:
            # Variable length - extend payload as needed
            required_size = start_pos + len(value)
            if len(payload) < required_size:
                payload.extend(b'\x00' * (required_size - len(payload)))

            # Replace from start position
            payload[start_pos:start_pos + len(value)] = value
    
    def _set_fixed_field(self, instance: HciPacketBase, value: Any):
        """Set fixed-size field"""
        payload = instance.payload
        start_pos = self._resolve_position(self.start, instance)
        if start_pos is None:
            return

        if self.end is not None:
            end_pos = self._resolve_position(self.end, instance)
            if end_pos is None:
                return
        else:
            end_pos = start_pos + (self.size or 1)

        # Ensure payload is large enough
        if len(payload) < end_pos:
            payload.extend(b'\x00' * (end_pos - len(payload)))

        # Pack and store the value
        try:
            packed_value = struct.pack(self.fmt, value)
            payload[start_pos:start_pos + len(packed_value)] = packed_value
        except struct.error as e:
            raise ValueError(f"Cannot pack value {value} with format {self.fmt}: {e}")
