# radar_data_reader/core/structures/d1553_structures.py

"""
Contains all ctypes structure definitions and dataclass wrappers related
to MIL-STD-1553 avionics data, typically found in D1553 or specific SOFT blocks.
"""

import ctypes
import math
from dataclasses import dataclass
from typing import Optional

from ...utils import logger
from .base_structures import CtypesStructureBase, BaseBlock

log = logger.get_logger(__name__)

# ==================================================================================================
# --- D1553 Constants and Sub-Structures ---
# ==================================================================================================

# Constants for converting raw integer values to physical units
ICD1553_GEOPOS_DEG_LSB = (4.65661e-10) * 180.0
ICD1553_SEMICIRCLE_DEG_LSB = (3.05176e-05) * 180.0
ICD1553_BARO_ALT_METERS_LSB = 4.0 * (1.0 / 3.280839895)
ICD1553_VELOCITY_METERS_LSB = (3.81470e-06) * (1.0 / 3.280839895)


class AvionicsMessage(CtypesStructureBase):
    """
    Represents the raw layout of the avionics message store,
    containing arrays for different 1553 messages (a1, a2, ..., b1, b2, etc.).
    """

    _fields_ = [
        ("tag", ctypes.c_uint32),
        ("att", ctypes.c_uint32 * 9),
        ("a1", ctypes.c_uint16 * 32),
        ("a2", ctypes.c_uint16 * 32),
        ("a3", ctypes.c_uint16 * 32),
        ("a4", ctypes.c_uint16 * 32),
        ("a5", ctypes.c_uint16 * 32),
        ("a7", ctypes.c_uint16 * 32),
        ("a8", ctypes.c_uint16 * 32),
        ("x1", ctypes.c_uint16 * 32),
        ("btt", ctypes.c_uint32 * 32),
        ("b1", ctypes.c_uint16 * 32),
        ("b2", ctypes.c_uint16 * 32),
        ("b3", ctypes.c_uint16 * 32),
        ("b4", ctypes.c_uint16 * 32),
        ("b5", ctypes.c_uint16 * 32),
        ("b6", ctypes.c_uint16 * 32),
        ("b7", ctypes.c_uint16 * 32),
        ("b8", ctypes.c_uint16 * 32),
        ("b9", ctypes.c_uint16 * 32),
        ("b10", ctypes.c_uint16 * 32),
        ("b11", ctypes.c_uint16 * 32),
        ("b12", ctypes.c_uint16 * 32),
        ("b13", ctypes.c_uint16 * 32),
        ("b14", ctypes.c_uint16 * 32),
        ("b15", ctypes.c_uint16 * 32),
        ("b16", ctypes.c_uint16 * 32),
        ("b17", ctypes.c_uint16 * 32),
        ("b18", ctypes.c_uint16 * 32),
    ]


class D1553Payload(CtypesStructureBase):
    """The complete D1553 payload ctypes structure."""

    _fields_ = [("d", AvionicsMessage)]


# ==================================================================================================
# --- Helper Dataclasses for Internal Caching ---
# ==================================================================================================


@dataclass
class _D1553Cache:
    """Internal helper to cache all calculated data from a D1553 payload at once."""

    # Navigation Data
    mission_timestamp: Optional[str] = None
    latitude_deg: Optional[float] = None
    longitude_deg: Optional[float] = None
    baro_altitude_m: Optional[float] = None
    true_heading_deg: Optional[float] = None
    magnetic_heading_deg: Optional[float] = None
    platform_azimuth_deg: Optional[float] = None
    north_velocity_ms: Optional[float] = None
    east_velocity_ms: Optional[float] = None
    down_velocity_ms: Optional[float] = None
    ground_track_angle_deg: Optional[float] = None
    # Status Data
    range_scale_nm: Optional[int] = None
    raw_range_scale: Optional[int] = None


# ==================================================================================================
# --- Top-Level D1553 Dataclass Wrapper ---
# ==================================================================================================


@dataclass
class D1553Block(BaseBlock):
    """
    Dataclass wrapper for a parsed D1553 block.
    Provides a flat API of properties to access structured, calculated data.
    """

    is_valid: bool
    payload: Optional[D1553Payload] = None
    _cache: Optional[_D1553Cache] = None

    def _ensure_cache(self):
        """
        Calculates all values from the payload and populates the internal cache.
        This method is called once on the first access to any property.
        """
        if self._cache is not None:
            return

        if not (self.is_valid and self.payload):
            # Create an empty cache to prevent re-checking
            self._cache = _D1553Cache()
            return

        self._cache = _D1553Cache()
        p = self.payload.d

        # --- Populate Navigation Data ---
        try:
            date_word = p.a1[5]
            time_word = p.a1[6]
            year = 2022
            month = (date_word >> 6) & 0xF
            day = (date_word >> 10) & 0x3F
            total_seconds = time_word * 2
            h, rem = divmod(total_seconds, 3600)
            m, s = divmod(rem, 60)
            if 1 <= month <= 12 and 1 <= day <= 31 and 0 <= h <= 23:
                self._cache.mission_timestamp = (
                    f"{year:04d}-{month:02d}-{day:02d}T{h:02d}:{m:02d}:{s:02d}"
                )

            lat_raw = (p.a4[23] << 16) | p.a4[24]
            self._cache.latitude_deg = (
                ctypes.c_int32(lat_raw).value * ICD1553_GEOPOS_DEG_LSB
            )

            lon_raw = (p.a4[25] << 16) | p.a4[26]
            self._cache.longitude_deg = (
                ctypes.c_int32(lon_raw).value * ICD1553_GEOPOS_DEG_LSB
            )

            self._cache.baro_altitude_m = p.a4[9] * ICD1553_BARO_ALT_METERS_LSB
            self._cache.true_heading_deg = (
                ctypes.c_int16(p.a4[2]).value * ICD1553_SEMICIRCLE_DEG_LSB
            )
            self._cache.magnetic_heading_deg = (
                ctypes.c_int16(p.a4[3]).value * ICD1553_SEMICIRCLE_DEG_LSB
            )
            self._cache.platform_azimuth_deg = (
                ctypes.c_int16(p.a5[8]).value * ICD1553_SEMICIRCLE_DEG_LSB
            )

            east_vel_raw = (p.a5[4] << 16) | p.a5[5]
            north_vel_raw = (p.a5[2] << 16) | p.a5[3]
            down_vel_raw = (p.a5[6] << 16) | p.a5[7]
            self._cache.east_velocity_ms = (
                ctypes.c_int32(east_vel_raw).value * ICD1553_VELOCITY_METERS_LSB
            )
            self._cache.north_velocity_ms = (
                ctypes.c_int32(north_vel_raw).value * ICD1553_VELOCITY_METERS_LSB
            )
            self._cache.down_velocity_ms = (
                ctypes.c_int32(down_vel_raw).value * ICD1553_VELOCITY_METERS_LSB
            )

            if (
                self._cache.east_velocity_ms is not None
                and self._cache.north_velocity_ms is not None
            ):
                track_rad = math.atan2(
                    self._cache.east_velocity_ms, self._cache.north_velocity_ms
                )
                self._cache.ground_track_angle_deg = math.degrees(track_rad)
        except (IndexError, TypeError) as e:
            log.warning(f"Error calculating D1553 navigation data: {e}")

        # --- Populate Status Data ---
        try:
            b7_word1 = p.b7[1]
            raw_scale = (b7_word1 >> 6) & 0x3
            self._cache.raw_range_scale = raw_scale
            scale_map = {0: 80, 1: 40, 2: 20, 3: 10}
            self._cache.range_scale_nm = scale_map.get(raw_scale)
        except IndexError as e:
            log.warning(f"Error calculating D1553 status data: {e}")

    # --- Public Properties (Flat API) ---
    @property
    def mission_timestamp_str(self) -> Optional[str]:
        self._ensure_cache()
        return self._cache.mission_timestamp

    @property
    def latitude_deg(self) -> Optional[float]:
        self._ensure_cache()
        return self._cache.latitude_deg

    @property
    def longitude_deg(self) -> Optional[float]:
        self._ensure_cache()
        return self._cache.longitude_deg

    @property
    def baro_altitude_m(self) -> Optional[float]:
        self._ensure_cache()
        return self._cache.baro_altitude_m

    @property
    def true_heading_deg(self) -> Optional[float]:
        self._ensure_cache()
        return self._cache.true_heading_deg

    @property
    def magnetic_heading_deg(self) -> Optional[float]:
        self._ensure_cache()
        return self._cache.magnetic_heading_deg

    @property
    def platform_azimuth_deg(self) -> Optional[float]:
        self._ensure_cache()
        return self._cache.platform_azimuth_deg

    @property
    def north_velocity_ms(self) -> Optional[float]:
        self._ensure_cache()
        return self._cache.north_velocity_ms

    @property
    def east_velocity_ms(self) -> Optional[float]:
        self._ensure_cache()
        return self._cache.east_velocity_ms

    @property
    def down_velocity_ms(self) -> Optional[float]:
        self._ensure_cache()
        return self._cache.down_velocity_ms

    @property
    def ground_track_angle_deg(self) -> Optional[float]:
        self._ensure_cache()
        return self._cache.ground_track_angle_deg

    @property
    def range_scale_nm(self) -> Optional[int]:
        self._ensure_cache()
        return self._cache.range_scale_nm

    @property
    def raw_range_scale(self) -> Optional[int]:
        self._ensure_cache()
        return self._cache.raw_range_scale

    @property
    def raw_mode(self) -> Optional[int]:
        """
        Returns the raw master mode value from the B7 message.
        Note: The C++ code uses a global r_mode, which seems to come
        from the DSPHDRIN. This property exposes the value potentially
        present in the 1553 message itself for comparison.
        The bits for this are an assumption based on typical avionics.
        Let's assume it's in b7[0], high bits.
        """
        self._ensure_cache()
        try:
            # This is an assumption, needs validation with real data.
            # From C++: unsigned int mode=x->b7[0]>>12;
            return self.payload.d.b7[0] >> 12
        except (TypeError, IndexError):
            return None

    @property
    def raw_range_scale(self) -> Optional[int]:
        self._ensure_cache()
        return self._cache.raw_range_scale
